diff --git a/lib/Cake/Cache/Cache.php b/lib/Cake/Cache/Cache.php index 3db66c4f..53e5ce5d 100755 --- a/lib/Cake/Cache/Cache.php +++ b/lib/Cake/Cache/Cache.php @@ -27,9 +27,9 @@ * be * * ``` - * Cache::config('shared', array( - * 'engine' => 'Apc', - * 'prefix' => 'my_app_' + * Cache::config('shared', array( + * 'engine' => 'Apc', + * 'prefix' => 'my_app_' * )); * ``` * @@ -40,594 +40,614 @@ * * @package Cake.Cache */ -class Cache { - -/** - * Cache configuration stack - * Keeps the permanent/default settings for each cache engine. - * These settings are used to reset the engines after temporary modification. - * - * @var array - */ - protected static $_config = array(); - -/** - * Group to Config mapping - * - * @var array - */ - protected static $_groups = array(); - -/** - * Whether to reset the settings with the next call to Cache::set(); - * - * @var array - */ - protected static $_reset = false; - -/** - * Engine instances keyed by configuration name. - * - * @var array - */ - protected static $_engines = array(); - -/** - * Set the cache configuration to use. config() can - * both create new configurations, return the settings for already configured - * configurations. - * - * To create a new configuration, or to modify an existing configuration permanently: - * - * `Cache::config('my_config', array('engine' => 'File', 'path' => TMP));` - * - * If you need to modify a configuration temporarily, use Cache::set(). - * To get the settings for a configuration: - * - * `Cache::config('default');` - * - * There are 5 built-in caching engines: - * - * - `FileEngine` - Uses simple files to store content. Poor performance, but good for - * storing large objects, or things that are not IO sensitive. - * - `ApcEngine` - Uses the APC object cache, one of the fastest caching engines. - * - `MemcacheEngine` - Uses the PECL::Memcache extension and Memcached for storage. - * Fast reads/writes, and benefits from memcache being distributed. - * - `XcacheEngine` - Uses the Xcache extension, an alternative to APC. - * - `WincacheEngine` - Uses Windows Cache Extension for PHP. Supports wincache 1.1.0 and higher. - * - * The following keys are used in core cache engines: - * - * - `duration` Specify how long items in this cache configuration last. - * - `groups` List of groups or 'tags' associated to every key stored in this config. - * handy for deleting a complete group from cache. - * - `prefix` Prefix appended to all entries. Good for when you need to share a keyspace - * with either another cache config or another application. - * - `probability` Probability of hitting a cache gc cleanup. Setting to 0 will disable - * cache::gc from ever being called automatically. - * - `servers' Used by memcache. Give the address of the memcached servers to use. - * - `compress` Used by memcache. Enables memcache's compressed format. - * - `serialize` Used by FileCache. Should cache objects be serialized first. - * - `path` Used by FileCache. Path to where cachefiles should be saved. - * - `lock` Used by FileCache. Should files be locked before writing to them? - * - `user` Used by Xcache. Username for XCache - * - `password` Used by Xcache/Redis. Password for XCache/Redis - * - * @param string $name Name of the configuration - * @param array $settings Optional associative array of settings passed to the engine - * @return array array(engine, settings) on success, false on failure - * @throws CacheException - * @see app/Config/core.php for configuration settings - */ - public static function config($name = null, $settings = array()) { - if (is_array($name)) { - $settings = $name; - } - - $current = array(); - if (isset(static::$_config[$name])) { - $current = static::$_config[$name]; - } - - if (!empty($settings)) { - static::$_config[$name] = $settings + $current; - } - - if (empty(static::$_config[$name]['engine'])) { - return false; - } - - if (!empty(static::$_config[$name]['groups'])) { - foreach (static::$_config[$name]['groups'] as $group) { - static::$_groups[$group][] = $name; - sort(static::$_groups[$group]); - static::$_groups[$group] = array_unique(static::$_groups[$group]); - } - } - - $engine = static::$_config[$name]['engine']; - - if (!isset(static::$_engines[$name])) { - static::_buildEngine($name); - $settings = static::$_config[$name] = static::settings($name); - } elseif ($settings = static::set(static::$_config[$name], null, $name)) { - static::$_config[$name] = $settings; - } - return compact('engine', 'settings'); - } - -/** - * Finds and builds the instance of the required engine class. - * - * @param string $name Name of the config array that needs an engine instance built - * @return bool - * @throws CacheException - */ - protected static function _buildEngine($name) { - $config = static::$_config[$name]; - - list($plugin, $class) = pluginSplit($config['engine'], true); - $cacheClass = $class . 'Engine'; - App::uses($cacheClass, $plugin . 'Cache/Engine'); - if (!class_exists($cacheClass)) { - throw new CacheException(__d('cake_dev', 'Cache engine %s is not available.', $name)); - } - $cacheClass = $class . 'Engine'; - if (!is_subclass_of($cacheClass, 'CacheEngine')) { - throw new CacheException(__d('cake_dev', 'Cache engines must use %s as a base class.', 'CacheEngine')); - } - static::$_engines[$name] = new $cacheClass(); - if (!static::$_engines[$name]->init($config)) { - $msg = __d( - 'cake_dev', - 'Cache engine "%s" is not properly configured. Ensure required extensions are installed, and credentials/permissions are correct', - $name - ); - throw new CacheException($msg); - } - if (static::$_engines[$name]->settings['probability'] && time() % static::$_engines[$name]->settings['probability'] === 0) { - static::$_engines[$name]->gc(); - } - return true; - } - -/** - * Returns an array containing the currently configured Cache settings. - * - * @return array Array of configured Cache config names. - */ - public static function configured() { - return array_keys(static::$_config); - } - -/** - * Drops a cache engine. Deletes the cache configuration information - * If the deleted configuration is the last configuration using a certain engine, - * the Engine instance is also unset. - * - * @param string $name A currently configured cache config you wish to remove. - * @return bool success of the removal, returns false when the config does not exist. - */ - public static function drop($name) { - if (!isset(static::$_config[$name])) { - return false; - } - unset(static::$_config[$name], static::$_engines[$name]); - return true; - } - -/** - * Temporarily change the settings on a cache config. The settings will persist for the next write - * operation (write, decrement, increment, clear). Any reads that are done before the write, will - * use the modified settings. If `$settings` is empty, the settings will be reset to the - * original configuration. - * - * Can be called with 2 or 3 parameters. To set multiple values at once. - * - * `Cache::set(array('duration' => '+30 minutes'), 'my_config');` - * - * Or to set one value. - * - * `Cache::set('duration', '+30 minutes', 'my_config');` - * - * To reset a config back to the originally configured values. - * - * `Cache::set(null, 'my_config');` - * - * @param string|array $settings Optional string for simple name-value pair or array - * @param string $value Optional for a simple name-value pair - * @param string $config The configuration name you are changing. Defaults to 'default' - * @return array Array of settings. - */ - public static function set($settings = array(), $value = null, $config = 'default') { - if (is_array($settings) && $value !== null) { - $config = $value; - } - if (!isset(static::$_config[$config]) || !isset(static::$_engines[$config])) { - return false; - } - if (!empty($settings)) { - static::$_reset = true; - } - - if (static::$_reset === true) { - if (empty($settings)) { - static::$_reset = false; - $settings = static::$_config[$config]; - } else { - if (is_string($settings) && $value !== null) { - $settings = array($settings => $value); - } - $settings += static::$_config[$config]; - if (isset($settings['duration']) && !is_numeric($settings['duration'])) { - $settings['duration'] = strtotime($settings['duration']) - time(); - } - } - static::$_engines[$config]->settings = $settings; - } - return static::settings($config); - } - -/** - * Garbage collection - * - * Permanently remove all expired and deleted data - * - * @param string $config [optional] The config name you wish to have garbage collected. Defaults to 'default' - * @param int $expires [optional] An expires timestamp. Defaults to NULL - * @return bool - */ - public static function gc($config = 'default', $expires = null) { - return static::$_engines[$config]->gc($expires); - } - -/** - * Write data for key into a cache engine. - * - * ### Usage: - * - * Writing to the active cache config: - * - * `Cache::write('cached_data', $data);` - * - * Writing to a specific cache config: - * - * `Cache::write('cached_data', $data, 'long_term');` - * - * @param string $key Identifier for the data - * @param mixed $value Data to be cached - anything except a resource - * @param string $config Optional string configuration name to write to. Defaults to 'default' - * @return bool True if the data was successfully cached, false on failure - */ - public static function write($key, $value, $config = 'default') { - $settings = static::settings($config); - - if (empty($settings)) { - return false; - } - if (!static::isInitialized($config)) { - return false; - } - $key = static::$_engines[$config]->key($key); - - if (!$key || is_resource($value)) { - return false; - } - - $success = static::$_engines[$config]->write($settings['prefix'] . $key, $value, $settings['duration']); - static::set(null, $config); - if ($success === false && $value !== '') { - trigger_error( - __d('cake_dev', - "%s cache was unable to write '%s' to %s cache", - $config, - $key, - static::$_engines[$config]->settings['engine'] - ), - E_USER_WARNING - ); - } - return $success; - } - -/** - * Read a key from a cache config. - * - * ### Usage: - * - * Reading from the active cache configuration. - * - * `Cache::read('my_data');` - * - * Reading from a specific cache configuration. - * - * `Cache::read('my_data', 'long_term');` - * - * @param string $key Identifier for the data - * @param string $config optional name of the configuration to use. Defaults to 'default' - * @return mixed The cached data, or false if the data doesn't exist, has expired, or if there was an error fetching it - */ - public static function read($key, $config = 'default') { - $settings = static::settings($config); - - if (empty($settings)) { - return false; - } - if (!static::isInitialized($config)) { - return false; - } - $key = static::$_engines[$config]->key($key); - if (!$key) { - return false; - } - return static::$_engines[$config]->read($settings['prefix'] . $key); - } - -/** - * Increment a number under the key and return incremented value. - * - * @param string $key Identifier for the data - * @param int $offset How much to add - * @param string $config Optional string configuration name. Defaults to 'default' - * @return mixed new value, or false if the data doesn't exist, is not integer, - * or if there was an error fetching it. - */ - public static function increment($key, $offset = 1, $config = 'default') { - $settings = static::settings($config); - - if (empty($settings)) { - return false; - } - if (!static::isInitialized($config)) { - return false; - } - $key = static::$_engines[$config]->key($key); - - if (!$key || !is_int($offset) || $offset < 0) { - return false; - } - $success = static::$_engines[$config]->increment($settings['prefix'] . $key, $offset); - static::set(null, $config); - return $success; - } - -/** - * Decrement a number under the key and return decremented value. - * - * @param string $key Identifier for the data - * @param int $offset How much to subtract - * @param string $config Optional string configuration name. Defaults to 'default' - * @return mixed new value, or false if the data doesn't exist, is not integer, - * or if there was an error fetching it - */ - public static function decrement($key, $offset = 1, $config = 'default') { - $settings = static::settings($config); - - if (empty($settings)) { - return false; - } - if (!static::isInitialized($config)) { - return false; - } - $key = static::$_engines[$config]->key($key); - - if (!$key || !is_int($offset) || $offset < 0) { - return false; - } - $success = static::$_engines[$config]->decrement($settings['prefix'] . $key, $offset); - static::set(null, $config); - return $success; - } - -/** - * Delete a key from the cache. - * - * ### Usage: - * - * Deleting from the active cache configuration. - * - * `Cache::delete('my_data');` - * - * Deleting from a specific cache configuration. - * - * `Cache::delete('my_data', 'long_term');` - * - * @param string $key Identifier for the data - * @param string $config name of the configuration to use. Defaults to 'default' - * @return bool True if the value was successfully deleted, false if it didn't exist or couldn't be removed - */ - public static function delete($key, $config = 'default') { - $settings = static::settings($config); - - if (empty($settings)) { - return false; - } - if (!static::isInitialized($config)) { - return false; - } - $key = static::$_engines[$config]->key($key); - if (!$key) { - return false; - } - - $success = static::$_engines[$config]->delete($settings['prefix'] . $key); - static::set(null, $config); - return $success; - } - -/** - * Delete all keys from the cache. - * - * @param bool $check if true will check expiration, otherwise delete all - * @param string $config name of the configuration to use. Defaults to 'default' - * @return bool True if the cache was successfully cleared, false otherwise - */ - public static function clear($check = false, $config = 'default') { - if (!static::isInitialized($config)) { - return false; - } - $success = static::$_engines[$config]->clear($check); - static::set(null, $config); - return $success; - } - -/** - * Delete all keys from the cache belonging to the same group. - * - * @param string $group name of the group to be cleared - * @param string $config name of the configuration to use. Defaults to 'default' - * @return bool True if the cache group was successfully cleared, false otherwise - */ - public static function clearGroup($group, $config = 'default') { - if (!static::isInitialized($config)) { - return false; - } - $success = static::$_engines[$config]->clearGroup($group); - static::set(null, $config); - return $success; - } - -/** - * Check if Cache has initialized a working config for the given name. - * - * @param string $config name of the configuration to use. Defaults to 'default' - * @return bool Whether or not the config name has been initialized. - */ - public static function isInitialized($config = 'default') { - if (Configure::read('Cache.disable')) { - return false; - } - return isset(static::$_engines[$config]); - } - -/** - * Return the settings for the named cache engine. - * - * @param string $name Name of the configuration to get settings for. Defaults to 'default' - * @return array list of settings for this engine - * @see Cache::config() - */ - public static function settings($name = 'default') { - if (!empty(static::$_engines[$name])) { - return static::$_engines[$name]->settings(); - } - return array(); - } - -/** - * Retrieve group names to config mapping. - * - * ``` - * Cache::config('daily', array( - * 'duration' => '1 day', 'groups' => array('posts') - * )); - * Cache::config('weekly', array( - * 'duration' => '1 week', 'groups' => array('posts', 'archive') - * )); - * $configs = Cache::groupConfigs('posts'); - * ``` - * - * $config will equal to `array('posts' => array('daily', 'weekly'))` - * - * @param string $group group name or null to retrieve all group mappings - * @return array map of group and all configuration that has the same group - * @throws CacheException - */ - public static function groupConfigs($group = null) { - if ($group === null) { - return static::$_groups; - } - if (isset(static::$_groups[$group])) { - return array($group => static::$_groups[$group]); - } - throw new CacheException(__d('cake_dev', 'Invalid cache group %s', $group)); - } - -/** - * Provides the ability to easily do read-through caching. - * - * When called if the $key is not set in $config, the $callable function - * will be invoked. The results will then be stored into the cache config - * at key. - * - * Examples: - * - * Using a Closure to provide data, assume $this is a Model: - * - * ``` - * $model = $this; - * $results = Cache::remember('all_articles', function() use ($model) { - * return $model->find('all'); - * }); - * ``` - * - * @param string $key The cache key to read/store data at. - * @param callable $callable The callable that provides data in the case when - * the cache key is empty. Can be any callable type supported by your PHP. - * @param string $config The cache configuration to use for this operation. - * Defaults to default. - * @return mixed The results of the callable or unserialized results. - */ - public static function remember($key, $callable, $config = 'default') { - $existing = static::read($key, $config); - if ($existing !== false) { - return $existing; - } - $results = call_user_func($callable); - static::write($key, $results, $config); - return $results; - } - -/** - * Write data for key into a cache engine if it doesn't exist already. - * - * ### Usage: - * - * Writing to the active cache config: - * - * `Cache::add('cached_data', $data);` - * - * Writing to a specific cache config: - * - * `Cache::add('cached_data', $data, 'long_term');` - * - * @param string $key Identifier for the data. - * @param mixed $value Data to be cached - anything except a resource. - * @param string $config Optional string configuration name to write to. Defaults to 'default'. - * @return bool True if the data was successfully cached, false on failure. - * Or if the key existed already. - */ - public static function add($key, $value, $config = 'default') { - $settings = self::settings($config); - - if (empty($settings)) { - return false; - } - if (!self::isInitialized($config)) { - return false; - } - $key = self::$_engines[$config]->key($key); - - if (!$key || is_resource($value)) { - return false; - } - - $success = self::$_engines[$config]->add($settings['prefix'] . $key, $value, $settings['duration']); - self::set(null, $config); - return $success; - } - -/** - * Fetch the engine attached to a specific configuration name. - * - * @param string $config Optional string configuration name to get an engine for. Defaults to 'default'. - * @return null|CacheEngine Null if the engine has not been initialized or the engine. - */ - public static function engine($config = 'default') { - if (self::isInitialized($config)) { - return self::$_engines[$config]; - } - - return null; - } +class Cache +{ + + /** + * Cache configuration stack + * Keeps the permanent/default settings for each cache engine. + * These settings are used to reset the engines after temporary modification. + * + * @var array + */ + protected static $_config = []; + + /** + * Group to Config mapping + * + * @var array + */ + protected static $_groups = []; + + /** + * Whether to reset the settings with the next call to Cache::set(); + * + * @var array + */ + protected static $_reset = false; + + /** + * Engine instances keyed by configuration name. + * + * @var array + */ + protected static $_engines = []; + + /** + * Set the cache configuration to use. config() can + * both create new configurations, return the settings for already configured + * configurations. + * + * To create a new configuration, or to modify an existing configuration permanently: + * + * `Cache::config('my_config', array('engine' => 'File', 'path' => TMP));` + * + * If you need to modify a configuration temporarily, use Cache::set(). + * To get the settings for a configuration: + * + * `Cache::config('default');` + * + * There are 5 built-in caching engines: + * + * - `FileEngine` - Uses simple files to store content. Poor performance, but good for + * storing large objects, or things that are not IO sensitive. + * - `ApcEngine` - Uses the APC object cache, one of the fastest caching engines. + * - `MemcacheEngine` - Uses the PECL::Memcache extension and Memcached for storage. + * Fast reads/writes, and benefits from memcache being distributed. + * - `XcacheEngine` - Uses the Xcache extension, an alternative to APC. + * - `WincacheEngine` - Uses Windows Cache Extension for PHP. Supports wincache 1.1.0 and higher. + * + * The following keys are used in core cache engines: + * + * - `duration` Specify how long items in this cache configuration last. + * - `groups` List of groups or 'tags' associated to every key stored in this config. + * handy for deleting a complete group from cache. + * - `prefix` Prefix appended to all entries. Good for when you need to share a keyspace + * with either another cache config or another application. + * - `probability` Probability of hitting a cache gc cleanup. Setting to 0 will disable + * cache::gc from ever being called automatically. + * - `servers' Used by memcache. Give the address of the memcached servers to use. + * - `compress` Used by memcache. Enables memcache's compressed format. + * - `serialize` Used by FileCache. Should cache objects be serialized first. + * - `path` Used by FileCache. Path to where cachefiles should be saved. + * - `lock` Used by FileCache. Should files be locked before writing to them? + * - `user` Used by Xcache. Username for XCache + * - `password` Used by Xcache/Redis. Password for XCache/Redis + * + * @param string $name Name of the configuration + * @param array $settings Optional associative array of settings passed to the engine + * @return array array(engine, settings) on success, false on failure + * @throws CacheException + * @see app/Config/core.php for configuration settings + */ + public static function config($name = null, $settings = []) + { + if (is_array($name)) { + $settings = $name; + } + + $current = []; + if (isset(static::$_config[$name])) { + $current = static::$_config[$name]; + } + + if (!empty($settings)) { + static::$_config[$name] = $settings + $current; + } + + if (empty(static::$_config[$name]['engine'])) { + return false; + } + + if (!empty(static::$_config[$name]['groups'])) { + foreach (static::$_config[$name]['groups'] as $group) { + static::$_groups[$group][] = $name; + sort(static::$_groups[$group]); + static::$_groups[$group] = array_unique(static::$_groups[$group]); + } + } + + $engine = static::$_config[$name]['engine']; + + if (!isset(static::$_engines[$name])) { + static::_buildEngine($name); + $settings = static::$_config[$name] = static::settings($name); + } else if ($settings = static::set(static::$_config[$name], null, $name)) { + static::$_config[$name] = $settings; + } + return compact('engine', 'settings'); + } + + /** + * Finds and builds the instance of the required engine class. + * + * @param string $name Name of the config array that needs an engine instance built + * @return bool + * @throws CacheException + */ + protected static function _buildEngine($name) + { + $config = static::$_config[$name]; + + list($plugin, $class) = pluginSplit($config['engine'], true); + $cacheClass = $class . 'Engine'; + App::uses($cacheClass, $plugin . 'Cache/Engine'); + if (!class_exists($cacheClass)) { + throw new CacheException(__d('cake_dev', 'Cache engine %s is not available.', $name)); + } + $cacheClass = $class . 'Engine'; + if (!is_subclass_of($cacheClass, 'CacheEngine')) { + throw new CacheException(__d('cake_dev', 'Cache engines must use %s as a base class.', 'CacheEngine')); + } + static::$_engines[$name] = new $cacheClass(); + if (!static::$_engines[$name]->init($config)) { + $msg = __d( + 'cake_dev', + 'Cache engine "%s" is not properly configured. Ensure required extensions are installed, and credentials/permissions are correct', + $name + ); + throw new CacheException($msg); + } + if (static::$_engines[$name]->settings['probability'] && time() % static::$_engines[$name]->settings['probability'] === 0) { + static::$_engines[$name]->gc(); + } + return true; + } + + /** + * Return the settings for the named cache engine. + * + * @param string $name Name of the configuration to get settings for. Defaults to 'default' + * @return array list of settings for this engine + * @see Cache::config() + */ + public static function settings($name = 'default') + { + if (!empty(static::$_engines[$name])) { + return static::$_engines[$name]->settings(); + } + return []; + } + + /** + * Temporarily change the settings on a cache config. The settings will persist for the next write + * operation (write, decrement, increment, clear). Any reads that are done before the write, will + * use the modified settings. If `$settings` is empty, the settings will be reset to the + * original configuration. + * + * Can be called with 2 or 3 parameters. To set multiple values at once. + * + * `Cache::set(array('duration' => '+30 minutes'), 'my_config');` + * + * Or to set one value. + * + * `Cache::set('duration', '+30 minutes', 'my_config');` + * + * To reset a config back to the originally configured values. + * + * `Cache::set(null, 'my_config');` + * + * @param string|array $settings Optional string for simple name-value pair or array + * @param string $value Optional for a simple name-value pair + * @param string $config The configuration name you are changing. Defaults to 'default' + * @return array Array of settings. + */ + public static function set($settings = [], $value = null, $config = 'default') + { + if (is_array($settings) && $value !== null) { + $config = $value; + } + if (!isset(static::$_config[$config]) || !isset(static::$_engines[$config])) { + return false; + } + if (!empty($settings)) { + static::$_reset = true; + } + + if (static::$_reset === true) { + if (empty($settings)) { + static::$_reset = false; + $settings = static::$_config[$config]; + } else { + if (is_string($settings) && $value !== null) { + $settings = [$settings => $value]; + } + $settings += static::$_config[$config]; + if (isset($settings['duration']) && !is_numeric($settings['duration'])) { + $settings['duration'] = strtotime($settings['duration']) - time(); + } + } + static::$_engines[$config]->settings = $settings; + } + return static::settings($config); + } + + /** + * Returns an array containing the currently configured Cache settings. + * + * @return array Array of configured Cache config names. + */ + public static function configured() + { + return array_keys(static::$_config); + } + + /** + * Drops a cache engine. Deletes the cache configuration information + * If the deleted configuration is the last configuration using a certain engine, + * the Engine instance is also unset. + * + * @param string $name A currently configured cache config you wish to remove. + * @return bool success of the removal, returns false when the config does not exist. + */ + public static function drop($name) + { + if (!isset(static::$_config[$name])) { + return false; + } + unset(static::$_config[$name], static::$_engines[$name]); + return true; + } + + /** + * Garbage collection + * + * Permanently remove all expired and deleted data + * + * @param string $config [optional] The config name you wish to have garbage collected. Defaults to 'default' + * @param int $expires [optional] An expires timestamp. Defaults to NULL + * @return bool + */ + public static function gc($config = 'default', $expires = null) + { + return static::$_engines[$config]->gc($expires); + } + + /** + * Increment a number under the key and return incremented value. + * + * @param string $key Identifier for the data + * @param int $offset How much to add + * @param string $config Optional string configuration name. Defaults to 'default' + * @return mixed new value, or false if the data doesn't exist, is not integer, + * or if there was an error fetching it. + */ + public static function increment($key, $offset = 1, $config = 'default') + { + $settings = static::settings($config); + + if (empty($settings)) { + return false; + } + if (!static::isInitialized($config)) { + return false; + } + $key = static::$_engines[$config]->key($key); + + if (!$key || !is_int($offset) || $offset < 0) { + return false; + } + $success = static::$_engines[$config]->increment($settings['prefix'] . $key, $offset); + static::set(null, $config); + return $success; + } + + /** + * Check if Cache has initialized a working config for the given name. + * + * @param string $config name of the configuration to use. Defaults to 'default' + * @return bool Whether or not the config name has been initialized. + */ + public static function isInitialized($config = 'default') + { + if (Configure::read('Cache.disable')) { + return false; + } + return isset(static::$_engines[$config]); + } + + /** + * Decrement a number under the key and return decremented value. + * + * @param string $key Identifier for the data + * @param int $offset How much to subtract + * @param string $config Optional string configuration name. Defaults to 'default' + * @return mixed new value, or false if the data doesn't exist, is not integer, + * or if there was an error fetching it + */ + public static function decrement($key, $offset = 1, $config = 'default') + { + $settings = static::settings($config); + + if (empty($settings)) { + return false; + } + if (!static::isInitialized($config)) { + return false; + } + $key = static::$_engines[$config]->key($key); + + if (!$key || !is_int($offset) || $offset < 0) { + return false; + } + $success = static::$_engines[$config]->decrement($settings['prefix'] . $key, $offset); + static::set(null, $config); + return $success; + } + + /** + * Delete a key from the cache. + * + * ### Usage: + * + * Deleting from the active cache configuration. + * + * `Cache::delete('my_data');` + * + * Deleting from a specific cache configuration. + * + * `Cache::delete('my_data', 'long_term');` + * + * @param string $key Identifier for the data + * @param string $config name of the configuration to use. Defaults to 'default' + * @return bool True if the value was successfully deleted, false if it didn't exist or couldn't be removed + */ + public static function delete($key, $config = 'default') + { + $settings = static::settings($config); + + if (empty($settings)) { + return false; + } + if (!static::isInitialized($config)) { + return false; + } + $key = static::$_engines[$config]->key($key); + if (!$key) { + return false; + } + + $success = static::$_engines[$config]->delete($settings['prefix'] . $key); + static::set(null, $config); + return $success; + } + + /** + * Delete all keys from the cache. + * + * @param bool $check if true will check expiration, otherwise delete all + * @param string $config name of the configuration to use. Defaults to 'default' + * @return bool True if the cache was successfully cleared, false otherwise + */ + public static function clear($check = false, $config = 'default') + { + if (!static::isInitialized($config)) { + return false; + } + $success = static::$_engines[$config]->clear($check); + static::set(null, $config); + return $success; + } + + /** + * Delete all keys from the cache belonging to the same group. + * + * @param string $group name of the group to be cleared + * @param string $config name of the configuration to use. Defaults to 'default' + * @return bool True if the cache group was successfully cleared, false otherwise + */ + public static function clearGroup($group, $config = 'default') + { + if (!static::isInitialized($config)) { + return false; + } + $success = static::$_engines[$config]->clearGroup($group); + static::set(null, $config); + return $success; + } + + /** + * Retrieve group names to config mapping. + * + * ``` + * Cache::config('daily', array( + * 'duration' => '1 day', 'groups' => array('posts') + * )); + * Cache::config('weekly', array( + * 'duration' => '1 week', 'groups' => array('posts', 'archive') + * )); + * $configs = Cache::groupConfigs('posts'); + * ``` + * + * $config will equal to `array('posts' => array('daily', 'weekly'))` + * + * @param string $group group name or null to retrieve all group mappings + * @return array map of group and all configuration that has the same group + * @throws CacheException + */ + public static function groupConfigs($group = null) + { + if ($group === null) { + return static::$_groups; + } + if (isset(static::$_groups[$group])) { + return [$group => static::$_groups[$group]]; + } + throw new CacheException(__d('cake_dev', 'Invalid cache group %s', $group)); + } + + /** + * Provides the ability to easily do read-through caching. + * + * When called if the $key is not set in $config, the $callable function + * will be invoked. The results will then be stored into the cache config + * at key. + * + * Examples: + * + * Using a Closure to provide data, assume $this is a Model: + * + * ``` + * $model = $this; + * $results = Cache::remember('all_articles', function() use ($model) { + * return $model->find('all'); + * }); + * ``` + * + * @param string $key The cache key to read/store data at. + * @param callable $callable The callable that provides data in the case when + * the cache key is empty. Can be any callable type supported by your PHP. + * @param string $config The cache configuration to use for this operation. + * Defaults to default. + * @return mixed The results of the callable or unserialized results. + */ + public static function remember($key, $callable, $config = 'default') + { + $existing = static::read($key, $config); + if ($existing !== false) { + return $existing; + } + $results = call_user_func($callable); + static::write($key, $results, $config); + return $results; + } + + /** + * Read a key from a cache config. + * + * ### Usage: + * + * Reading from the active cache configuration. + * + * `Cache::read('my_data');` + * + * Reading from a specific cache configuration. + * + * `Cache::read('my_data', 'long_term');` + * + * @param string $key Identifier for the data + * @param string $config optional name of the configuration to use. Defaults to 'default' + * @return mixed The cached data, or false if the data doesn't exist, has expired, or if there was an error fetching it + */ + public static function read($key, $config = 'default') + { + $settings = static::settings($config); + + if (empty($settings)) { + return false; + } + if (!static::isInitialized($config)) { + return false; + } + $key = static::$_engines[$config]->key($key); + if (!$key) { + return false; + } + return static::$_engines[$config]->read($settings['prefix'] . $key); + } + + /** + * Write data for key into a cache engine. + * + * ### Usage: + * + * Writing to the active cache config: + * + * `Cache::write('cached_data', $data);` + * + * Writing to a specific cache config: + * + * `Cache::write('cached_data', $data, 'long_term');` + * + * @param string $key Identifier for the data + * @param mixed $value Data to be cached - anything except a resource + * @param string $config Optional string configuration name to write to. Defaults to 'default' + * @return bool True if the data was successfully cached, false on failure + */ + public static function write($key, $value, $config = 'default') + { + $settings = static::settings($config); + + if (empty($settings)) { + return false; + } + if (!static::isInitialized($config)) { + return false; + } + $key = static::$_engines[$config]->key($key); + + if (!$key || is_resource($value)) { + return false; + } + + $success = static::$_engines[$config]->write($settings['prefix'] . $key, $value, $settings['duration']); + static::set(null, $config); + if ($success === false && $value !== '') { + trigger_error( + __d('cake_dev', + "%s cache was unable to write '%s' to %s cache", + $config, + $key, + static::$_engines[$config]->settings['engine'] + ), + E_USER_WARNING + ); + } + return $success; + } + + /** + * Write data for key into a cache engine if it doesn't exist already. + * + * ### Usage: + * + * Writing to the active cache config: + * + * `Cache::add('cached_data', $data);` + * + * Writing to a specific cache config: + * + * `Cache::add('cached_data', $data, 'long_term');` + * + * @param string $key Identifier for the data. + * @param mixed $value Data to be cached - anything except a resource. + * @param string $config Optional string configuration name to write to. Defaults to 'default'. + * @return bool True if the data was successfully cached, false on failure. + * Or if the key existed already. + */ + public static function add($key, $value, $config = 'default') + { + $settings = self::settings($config); + + if (empty($settings)) { + return false; + } + if (!self::isInitialized($config)) { + return false; + } + $key = self::$_engines[$config]->key($key); + + if (!$key || is_resource($value)) { + return false; + } + + $success = self::$_engines[$config]->add($settings['prefix'] . $key, $value, $settings['duration']); + self::set(null, $config); + return $success; + } + + /** + * Fetch the engine attached to a specific configuration name. + * + * @param string $config Optional string configuration name to get an engine for. Defaults to 'default'. + * @return null|CacheEngine Null if the engine has not been initialized or the engine. + */ + public static function engine($config = 'default') + { + if (self::isInitialized($config)) { + return self::$_engines[$config]; + } + + return null; + } } diff --git a/lib/Cake/Cache/CacheEngine.php b/lib/Cake/Cache/CacheEngine.php index 3e2c2563..e4f50d46 100755 --- a/lib/Cake/Cache/CacheEngine.php +++ b/lib/Cake/Cache/CacheEngine.php @@ -19,172 +19,180 @@ * * @package Cake.Cache */ -abstract class CacheEngine { - -/** - * Settings of current engine instance - * - * @var array - */ - public $settings = array(); - -/** - * Contains the compiled string with all groups - * prefixes to be prepended to every key in this cache engine - * - * @var string - */ - protected $_groupPrefix = null; - -/** - * Initialize the cache engine - * - * Called automatically by the cache frontend - * - * @param array $settings Associative array of parameters for the engine - * @return bool True if the engine has been successfully initialized, false if not - */ - public function init($settings = array()) { - $settings += $this->settings + array( - 'prefix' => 'cake_', - 'duration' => 3600, - 'probability' => 100, - 'groups' => array() - ); - $this->settings = $settings; - if (!empty($this->settings['groups'])) { - sort($this->settings['groups']); - $this->_groupPrefix = str_repeat('%s_', count($this->settings['groups'])); - } - if (!is_numeric($this->settings['duration'])) { - $this->settings['duration'] = strtotime($this->settings['duration']) - time(); - } - return true; - } - -/** - * Garbage collection - * - * Permanently remove all expired and deleted data - * - * @param int $expires [optional] An expires timestamp, invalidating all data before. - * @return void - */ - public function gc($expires = null) { - } - -/** - * Write value for a key into cache - * - * @param string $key Identifier for the data - * @param mixed $value Data to be cached - * @param int $duration How long to cache for. - * @return bool True if the data was successfully cached, false on failure - */ - abstract public function write($key, $value, $duration); - -/** - * Write value for a key into cache if it doesn't already exist - * - * @param string $key Identifier for the data - * @param mixed $value Data to be cached - * @param int $duration How long to cache for. - * @return bool True if the data was successfully cached, false on failure - */ - public function add($key, $value, $duration) { - } - -/** - * Read a key from the cache - * - * @param string $key Identifier for the data - * @return mixed The cached data, or false if the data doesn't exist, has expired, or if there was an error fetching it - */ - abstract public function read($key); - -/** - * Increment a number under the key and return incremented value - * - * @param string $key Identifier for the data - * @param int $offset How much to add - * @return New incremented value, false otherwise - */ - abstract public function increment($key, $offset = 1); - -/** - * Decrement a number under the key and return decremented value - * - * @param string $key Identifier for the data - * @param int $offset How much to subtract - * @return New incremented value, false otherwise - */ - abstract public function decrement($key, $offset = 1); - -/** - * Delete a key from the cache - * - * @param string $key Identifier for the data - * @return bool True if the value was successfully deleted, false if it didn't exist or couldn't be removed - */ - abstract public function delete($key); - -/** - * Delete all keys from the cache - * - * @param bool $check if true will check expiration, otherwise delete all - * @return bool True if the cache was successfully cleared, false otherwise - */ - abstract public function clear($check); - -/** - * Clears all values belonging to a group. Is up to the implementing engine - * to decide whether actually delete the keys or just simulate it to achieve - * the same result. - * - * @param string $group name of the group to be cleared - * @return bool - */ - public function clearGroup($group) { - return false; - } - -/** - * Does whatever initialization for each group is required - * and returns the `group value` for each of them, this is - * the token representing each group in the cache key - * - * @return array - */ - public function groups() { - return $this->settings['groups']; - } - -/** - * Cache Engine settings - * - * @return array settings - */ - public function settings() { - return $this->settings; - } - -/** - * Generates a safe key for use with cache engine storage engines. - * - * @param string $key the key passed over - * @return mixed string $key or false - */ - public function key($key) { - if (empty($key)) { - return false; - } - - $prefix = ''; - if (!empty($this->_groupPrefix)) { - $prefix = md5(implode('_', $this->groups())); - } - - $key = preg_replace('/[\s]+/', '_', strtolower(trim(str_replace(array(DS, '/', '.'), '_', strval($key))))); - return $prefix . $key; - } +abstract class CacheEngine +{ + + /** + * Settings of current engine instance + * + * @var array + */ + public $settings = []; + + /** + * Contains the compiled string with all groups + * prefixes to be prepended to every key in this cache engine + * + * @var string + */ + protected $_groupPrefix = null; + + /** + * Initialize the cache engine + * + * Called automatically by the cache frontend + * + * @param array $settings Associative array of parameters for the engine + * @return bool True if the engine has been successfully initialized, false if not + */ + public function init($settings = []) + { + $settings += $this->settings + [ + 'prefix' => 'cake_', + 'duration' => 3600, + 'probability' => 100, + 'groups' => [] + ]; + $this->settings = $settings; + if (!empty($this->settings['groups'])) { + sort($this->settings['groups']); + $this->_groupPrefix = str_repeat('%s_', count($this->settings['groups'])); + } + if (!is_numeric($this->settings['duration'])) { + $this->settings['duration'] = strtotime($this->settings['duration']) - time(); + } + return true; + } + + /** + * Garbage collection + * + * Permanently remove all expired and deleted data + * + * @param int $expires [optional] An expires timestamp, invalidating all data before. + * @return void + */ + public function gc($expires = null) + { + } + + /** + * Write value for a key into cache + * + * @param string $key Identifier for the data + * @param mixed $value Data to be cached + * @param int $duration How long to cache for. + * @return bool True if the data was successfully cached, false on failure + */ + abstract public function write($key, $value, $duration); + + /** + * Write value for a key into cache if it doesn't already exist + * + * @param string $key Identifier for the data + * @param mixed $value Data to be cached + * @param int $duration How long to cache for. + * @return bool True if the data was successfully cached, false on failure + */ + public function add($key, $value, $duration) + { + } + + /** + * Read a key from the cache + * + * @param string $key Identifier for the data + * @return mixed The cached data, or false if the data doesn't exist, has expired, or if there was an error fetching it + */ + abstract public function read($key); + + /** + * Increment a number under the key and return incremented value + * + * @param string $key Identifier for the data + * @param int $offset How much to add + * @return New incremented value, false otherwise + */ + abstract public function increment($key, $offset = 1); + + /** + * Decrement a number under the key and return decremented value + * + * @param string $key Identifier for the data + * @param int $offset How much to subtract + * @return New incremented value, false otherwise + */ + abstract public function decrement($key, $offset = 1); + + /** + * Delete a key from the cache + * + * @param string $key Identifier for the data + * @return bool True if the value was successfully deleted, false if it didn't exist or couldn't be removed + */ + abstract public function delete($key); + + /** + * Delete all keys from the cache + * + * @param bool $check if true will check expiration, otherwise delete all + * @return bool True if the cache was successfully cleared, false otherwise + */ + abstract public function clear($check); + + /** + * Clears all values belonging to a group. Is up to the implementing engine + * to decide whether actually delete the keys or just simulate it to achieve + * the same result. + * + * @param string $group name of the group to be cleared + * @return bool + */ + public function clearGroup($group) + { + return false; + } + + /** + * Cache Engine settings + * + * @return array settings + */ + public function settings() + { + return $this->settings; + } + + /** + * Generates a safe key for use with cache engine storage engines. + * + * @param string $key the key passed over + * @return mixed string $key or false + */ + public function key($key) + { + if (empty($key)) { + return false; + } + + $prefix = ''; + if (!empty($this->_groupPrefix)) { + $prefix = md5(implode('_', $this->groups())); + } + + $key = preg_replace('/[\s]+/', '_', strtolower(trim(str_replace([DS, '/', '.'], '_', strval($key))))); + return $prefix . $key; + } + + /** + * Does whatever initialization for each group is required + * and returns the `group value` for each of them, this is + * the token representing each group in the cache key + * + * @return array + */ + public function groups() + { + return $this->settings['groups']; + } } diff --git a/lib/Cake/Cache/Engine/ApcEngine.php b/lib/Cake/Cache/Engine/ApcEngine.php index 329e8719..f1828d28 100755 --- a/lib/Cake/Cache/Engine/ApcEngine.php +++ b/lib/Cake/Cache/Engine/ApcEngine.php @@ -21,210 +21,221 @@ * * @package Cake.Cache.Engine */ -class ApcEngine extends CacheEngine { +class ApcEngine extends CacheEngine +{ -/** - * Contains the compiled group names - * (prefixed with the global configuration prefix) - * - * @var array - */ - protected $_compiledGroupNames = array(); + /** + * Contains the compiled group names + * (prefixed with the global configuration prefix) + * + * @var array + */ + protected $_compiledGroupNames = []; -/** - * APC or APCu extension - * - * @var string - */ - protected $_apcExtension = 'apc'; + /** + * APC or APCu extension + * + * @var string + */ + protected $_apcExtension = 'apc'; -/** - * Initialize the Cache Engine - * - * Called automatically by the cache frontend - * To reinitialize the settings call Cache::engine('EngineName', [optional] settings = array()); - * - * @param array $settings array of setting for the engine - * @return bool True if the engine has been successfully initialized, false if not - * @see CacheEngine::__defaults - */ - public function init($settings = array()) { - if (!isset($settings['prefix'])) { - $settings['prefix'] = Inflector::slug(APP_DIR) . '_'; - } - $settings += array('engine' => 'Apc'); - parent::init($settings); - if (function_exists('apcu_dec')) { - $this->_apcExtension = 'apcu'; - return true; - } - return function_exists('apc_dec'); - } + /** + * Initialize the Cache Engine + * + * Called automatically by the cache frontend + * To reinitialize the settings call Cache::engine('EngineName', [optional] settings = array()); + * + * @param array $settings array of setting for the engine + * @return bool True if the engine has been successfully initialized, false if not + * @see CacheEngine::__defaults + */ + public function init($settings = []) + { + if (!isset($settings['prefix'])) { + $settings['prefix'] = Inflector::slug(APP_DIR) . '_'; + } + $settings += ['engine' => 'Apc']; + parent::init($settings); + if (function_exists('apcu_dec')) { + $this->_apcExtension = 'apcu'; + return true; + } + return function_exists('apc_dec'); + } -/** - * Write data for key into cache - * - * @param string $key Identifier for the data - * @param mixed $value Data to be cached - * @param int $duration How long to cache the data, in seconds - * @return bool True if the data was successfully cached, false on failure - */ - public function write($key, $value, $duration) { - $expires = 0; - if ($duration) { - $expires = time() + $duration; - } - $func = $this->_apcExtension . '_store'; - $func($key . '_expires', $expires, $duration); - return $func($key, $value, $duration); - } + /** + * Write data for key into cache + * + * @param string $key Identifier for the data + * @param mixed $value Data to be cached + * @param int $duration How long to cache the data, in seconds + * @return bool True if the data was successfully cached, false on failure + */ + public function write($key, $value, $duration) + { + $expires = 0; + if ($duration) { + $expires = time() + $duration; + } + $func = $this->_apcExtension . '_store'; + $func($key . '_expires', $expires, $duration); + return $func($key, $value, $duration); + } -/** - * Read a key from the cache - * - * @param string $key Identifier for the data - * @return mixed The cached data, or false if the data doesn't exist, has expired, or if there was an error fetching it - */ - public function read($key) { - $time = time(); - $func = $this->_apcExtension . '_fetch'; - $cachetime = (int)$func($key . '_expires'); - if ($cachetime !== 0 && ($cachetime < $time || ($time + $this->settings['duration']) < $cachetime)) { - return false; - } - return $func($key); - } + /** + * Read a key from the cache + * + * @param string $key Identifier for the data + * @return mixed The cached data, or false if the data doesn't exist, has expired, or if there was an error fetching it + */ + public function read($key) + { + $time = time(); + $func = $this->_apcExtension . '_fetch'; + $cachetime = (int)$func($key . '_expires'); + if ($cachetime !== 0 && ($cachetime < $time || ($time + $this->settings['duration']) < $cachetime)) { + return false; + } + return $func($key); + } -/** - * Increments the value of an integer cached key - * - * @param string $key Identifier for the data - * @param int $offset How much to increment - * @return New incremented value, false otherwise - */ - public function increment($key, $offset = 1) { - $func = $this->_apcExtension . '_inc'; - return $func($key, $offset); - } + /** + * Increments the value of an integer cached key + * + * @param string $key Identifier for the data + * @param int $offset How much to increment + * @return New incremented value, false otherwise + */ + public function increment($key, $offset = 1) + { + $func = $this->_apcExtension . '_inc'; + return $func($key, $offset); + } -/** - * Decrements the value of an integer cached key - * - * @param string $key Identifier for the data - * @param int $offset How much to subtract - * @return New decremented value, false otherwise - */ - public function decrement($key, $offset = 1) { - $func = $this->_apcExtension . '_dec'; - return $func($key, $offset); - } + /** + * Decrements the value of an integer cached key + * + * @param string $key Identifier for the data + * @param int $offset How much to subtract + * @return New decremented value, false otherwise + */ + public function decrement($key, $offset = 1) + { + $func = $this->_apcExtension . '_dec'; + return $func($key, $offset); + } -/** - * Delete a key from the cache - * - * @param string $key Identifier for the data - * @return bool True if the value was successfully deleted, false if it didn't exist or couldn't be removed - */ - public function delete($key) { - $func = $this->_apcExtension . '_delete'; - return $func($key); - } + /** + * Delete a key from the cache + * + * @param string $key Identifier for the data + * @return bool True if the value was successfully deleted, false if it didn't exist or couldn't be removed + */ + public function delete($key) + { + $func = $this->_apcExtension . '_delete'; + return $func($key); + } -/** - * Delete all keys from the cache. This will clear every cache config using APC. - * - * @param bool $check If true, nothing will be cleared, as entries are removed - * from APC as they expired. This flag is really only used by FileEngine. - * @return bool True Returns true. - */ - public function clear($check) { - if ($check) { - return true; - } - $func = $this->_apcExtension . '_delete'; - if (class_exists('APCIterator', false)) { - $iterator = new APCIterator( - 'user', - '/^' . preg_quote($this->settings['prefix'], '/') . '/', - APC_ITER_NONE - ); - $func($iterator); - return true; - } - $cache = $this->_apcExtension === 'apc' ? apc_cache_info('user') : apcu_cache_info(); - foreach ($cache['cache_list'] as $key) { - if (strpos($key['info'], $this->settings['prefix']) === 0) { - $func($key['info']); - } - } - return true; - } + /** + * Delete all keys from the cache. This will clear every cache config using APC. + * + * @param bool $check If true, nothing will be cleared, as entries are removed + * from APC as they expired. This flag is really only used by FileEngine. + * @return bool True Returns true. + */ + public function clear($check) + { + if ($check) { + return true; + } + $func = $this->_apcExtension . '_delete'; + if (class_exists('APCIterator', false)) { + $iterator = new APCIterator( + 'user', + '/^' . preg_quote($this->settings['prefix'], '/') . '/', + APC_ITER_NONE + ); + $func($iterator); + return true; + } + $cache = $this->_apcExtension === 'apc' ? apc_cache_info('user') : apcu_cache_info(); + foreach ($cache['cache_list'] as $key) { + if (strpos($key['info'], $this->settings['prefix']) === 0) { + $func($key['info']); + } + } + return true; + } -/** - * Returns the `group value` for each of the configured groups - * If the group initial value was not found, then it initializes - * the group accordingly. - * - * @return array - */ - public function groups() { - if (empty($this->_compiledGroupNames)) { - foreach ($this->settings['groups'] as $group) { - $this->_compiledGroupNames[] = $this->settings['prefix'] . $group; - } - } + /** + * Returns the `group value` for each of the configured groups + * If the group initial value was not found, then it initializes + * the group accordingly. + * + * @return array + */ + public function groups() + { + if (empty($this->_compiledGroupNames)) { + foreach ($this->settings['groups'] as $group) { + $this->_compiledGroupNames[] = $this->settings['prefix'] . $group; + } + } - $fetchFunc = $this->_apcExtension . '_fetch'; - $storeFunc = $this->_apcExtension . '_store'; - $groups = $fetchFunc($this->_compiledGroupNames); - if (count($groups) !== count($this->settings['groups'])) { - foreach ($this->_compiledGroupNames as $group) { - if (!isset($groups[$group])) { - $storeFunc($group, 1); - $groups[$group] = 1; - } - } - ksort($groups); - } + $fetchFunc = $this->_apcExtension . '_fetch'; + $storeFunc = $this->_apcExtension . '_store'; + $groups = $fetchFunc($this->_compiledGroupNames); + if (count($groups) !== count($this->settings['groups'])) { + foreach ($this->_compiledGroupNames as $group) { + if (!isset($groups[$group])) { + $storeFunc($group, 1); + $groups[$group] = 1; + } + } + ksort($groups); + } - $result = array(); - $groups = array_values($groups); - foreach ($this->settings['groups'] as $i => $group) { - $result[] = $group . $groups[$i]; - } - return $result; - } + $result = []; + $groups = array_values($groups); + foreach ($this->settings['groups'] as $i => $group) { + $result[] = $group . $groups[$i]; + } + return $result; + } -/** - * Increments the group value to simulate deletion of all keys under a group - * old values will remain in storage until they expire. - * - * @param string $group The group to clear. - * @return bool success - */ - public function clearGroup($group) { - $func = $this->_apcExtension . '_inc'; - $func($this->settings['prefix'] . $group, 1, $success); - return $success; - } + /** + * Increments the group value to simulate deletion of all keys under a group + * old values will remain in storage until they expire. + * + * @param string $group The group to clear. + * @return bool success + */ + public function clearGroup($group) + { + $func = $this->_apcExtension . '_inc'; + $func($this->settings['prefix'] . $group, 1, $success); + return $success; + } -/** - * Write data for key into cache if it doesn't exist already. - * If it already exists, it fails and returns false. - * - * @param string $key Identifier for the data. - * @param mixed $value Data to be cached. - * @param int $duration How long to cache the data, in seconds. - * @return bool True if the data was successfully cached, false on failure. - * @link http://php.net/manual/en/function.apc-add.php - */ - public function add($key, $value, $duration) { - $expires = 0; - if ($duration) { - $expires = time() + $duration; - } - $func = $this->_apcExtension . '_add'; - $func($key . '_expires', $expires, $duration); - return $func($key, $value, $duration); - } + /** + * Write data for key into cache if it doesn't exist already. + * If it already exists, it fails and returns false. + * + * @param string $key Identifier for the data. + * @param mixed $value Data to be cached. + * @param int $duration How long to cache the data, in seconds. + * @return bool True if the data was successfully cached, false on failure. + * @link http://php.net/manual/en/function.apc-add.php + */ + public function add($key, $value, $duration) + { + $expires = 0; + if ($duration) { + $expires = time() + $duration; + } + $func = $this->_apcExtension . '_add'; + $func($key . '_expires', $expires, $duration); + return $func($key, $value, $duration); + } } diff --git a/lib/Cake/Cache/Engine/FileEngine.php b/lib/Cake/Cache/Engine/FileEngine.php index ed982424..a43b8245 100755 --- a/lib/Cake/Cache/Engine/FileEngine.php +++ b/lib/Cake/Cache/Engine/FileEngine.php @@ -28,431 +28,444 @@ * * @package Cake.Cache.Engine */ -class FileEngine extends CacheEngine { +class FileEngine extends CacheEngine +{ + + /** + * Settings + * + * - path = absolute path to cache directory, default => CACHE + * - prefix = string prefix for filename, default => cake_ + * - lock = enable file locking on write, default => true + * - serialize = serialize the data, default => true + * + * @var array + * @see CacheEngine::__defaults + */ + public $settings = []; + /** + * Instance of SplFileObject class + * + * @var File + */ + protected $_File = null; + /** + * True unless FileEngine::__active(); fails + * + * @var bool + */ + protected $_init = true; + + /** + * Initialize the Cache Engine + * + * Called automatically by the cache frontend + * To reinitialize the settings call Cache::engine('EngineName', [optional] settings = array()); + * + * @param array $settings array of setting for the engine + * @return bool True if the engine has been successfully initialized, false if not + */ + public function init($settings = []) + { + $settings += [ + 'engine' => 'File', + 'path' => CACHE, + 'prefix' => 'cake_', + 'lock' => true, + 'serialize' => true, + 'isWindows' => false, + 'mask' => 0664 + ]; + parent::init($settings); + + if (DS === '\\') { + $this->settings['isWindows'] = true; + } + if (substr($this->settings['path'], -1) !== DS) { + $this->settings['path'] .= DS; + } + if (!empty($this->_groupPrefix)) { + $this->_groupPrefix = str_replace('_', DS, $this->_groupPrefix); + } + return $this->_active(); + } + + /** + * Determine is cache directory is writable + * + * @return bool + */ + protected function _active() + { + $dir = new SplFileInfo($this->settings['path']); + $path = $dir->getPathname(); + if (!is_dir($path)) { + if (!mkdir($path, 0775, true)) { + exit(file_get_contents(ROOT . '/lib/Cake/View/Errors/permissions_errors.ctp')); + return false; + } + } + if ($this->_init && !($dir->isDir() && $dir->isWritable())) { + $this->_init = false; + exit(file_get_contents(ROOT . '/lib/Cake/View/Errors/permissions_errors.ctp')); + return false; + } + return true; + } + + /** + * Garbage collection. Permanently remove all expired and deleted data + * + * @param int $expires [optional] An expires timestamp, invalidating all data before. + * @return bool True if garbage collection was successful, false on failure + */ + public function gc($expires = null) + { + return $this->clear(true); + } + + /** + * Delete all values from the cache + * + * @param bool $check Optional - only delete expired cache items + * @return bool True if the cache was successfully cleared, false otherwise + */ + public function clear($check) + { + if (!$this->_init) { + return false; + } + $this->_File = null; -/** - * Instance of SplFileObject class - * - * @var File - */ - protected $_File = null; + $threshold = $now = false; + if ($check) { + $now = time(); + $threshold = $now - $this->settings['duration']; + } -/** - * Settings - * - * - path = absolute path to cache directory, default => CACHE - * - prefix = string prefix for filename, default => cake_ - * - lock = enable file locking on write, default => true - * - serialize = serialize the data, default => true - * - * @var array - * @see CacheEngine::__defaults - */ - public $settings = array(); + $this->_clearDirectory($this->settings['path'], $now, $threshold); -/** - * True unless FileEngine::__active(); fails - * - * @var bool - */ - protected $_init = true; + $directory = new RecursiveDirectoryIterator($this->settings['path']); + $contents = new RecursiveIteratorIterator($directory, RecursiveIteratorIterator::SELF_FIRST); + $cleared = []; + foreach ($contents as $path) { + if ($path->isFile()) { + continue; + } -/** - * Initialize the Cache Engine - * - * Called automatically by the cache frontend - * To reinitialize the settings call Cache::engine('EngineName', [optional] settings = array()); - * - * @param array $settings array of setting for the engine - * @return bool True if the engine has been successfully initialized, false if not - */ - public function init($settings = array()) { - $settings += array( - 'engine' => 'File', - 'path' => CACHE, - 'prefix' => 'cake_', - 'lock' => true, - 'serialize' => true, - 'isWindows' => false, - 'mask' => 0664 - ); - parent::init($settings); - - if (DS === '\\') { - $this->settings['isWindows'] = true; - } - if (substr($this->settings['path'], -1) !== DS) { - $this->settings['path'] .= DS; - } - if (!empty($this->_groupPrefix)) { - $this->_groupPrefix = str_replace('_', DS, $this->_groupPrefix); - } - return $this->_active(); - } + $path = $path->getRealPath() . DS; + if (!in_array($path, $cleared)) { + $this->_clearDirectory($path, $now, $threshold); + $cleared[] = $path; + } + } + return true; + } + + /** + * Used to clear a directory of matching files. + * + * @param string $path The path to search. + * @param int $now The current timestamp + * @param int $threshold Any file not modified after this value will be deleted. + * @return void + */ + protected function _clearDirectory($path, $now, $threshold) + { + $prefixLength = strlen($this->settings['prefix']); -/** - * Garbage collection. Permanently remove all expired and deleted data - * - * @param int $expires [optional] An expires timestamp, invalidating all data before. - * @return bool True if garbage collection was successful, false on failure - */ - public function gc($expires = null) { - return $this->clear(true); - } + if (!is_dir($path)) { + return; + } -/** - * Write data for key into cache - * - * @param string $key Identifier for the data - * @param mixed $data Data to be cached - * @param int $duration How long to cache the data, in seconds - * @return bool True if the data was successfully cached, false on failure - */ - public function write($key, $data, $duration) { - if (!$this->_init) { - return false; - } + $dir = dir($path); + if ($dir === false) { + return; + } - if ($this->_setKey($key, true) === false) { - return false; - } + while (($entry = $dir->read()) !== false) { + if (substr($entry, 0, $prefixLength) !== $this->settings['prefix']) { + continue; + } - $lineBreak = "\n"; + try { + $file = new SplFileObject($path . $entry, 'r'); + } catch (Exception $e) { + continue; + } - if ($this->settings['isWindows']) { - $lineBreak = "\r\n"; - } + if ($threshold) { + $mtime = $file->getMTime(); - if (!empty($this->settings['serialize'])) { - if ($this->settings['isWindows']) { - $data = str_replace('\\', '\\\\\\\\', serialize($data)); - } else { - $data = serialize($data); - } - } + if ($mtime > $threshold) { + continue; + } + $expires = (int)$file->current(); - $expires = time() + $duration; - $contents = implode(array($expires, $lineBreak, $data, $lineBreak)); + if ($expires > $now) { + continue; + } + } + if ($file->isFile()) { + $filePath = $file->getRealPath(); + $file = null; - if ($this->settings['lock']) { - $this->_File->flock(LOCK_EX); - } + //@codingStandardsIgnoreStart + @unlink($filePath); + //@codingStandardsIgnoreEnd + } + } + } + + /** + * Delete a key from the cache + * + * @param string $key Identifier for the data + * @return bool True if the value was successfully deleted, false if it didn't exist or couldn't be removed + */ + public function delete($key) + { + if ($this->_setKey($key) === false || !$this->_init) { + return false; + } + $path = $this->_File->getRealPath(); + $this->_File = null; + + //@codingStandardsIgnoreStart + return @unlink($path); + //@codingStandardsIgnoreEnd + } + + /** + * Sets the current cache key this class is managing, and creates a writable SplFileObject + * for the cache file the key is referring to. + * + * @param string $key The key + * @param bool $createKey Whether the key should be created if it doesn't exists, or not + * @return bool true if the cache key could be set, false otherwise + */ + protected function _setKey($key, $createKey = false) + { + $groups = null; + if (!empty($this->_groupPrefix)) { + $groups = vsprintf($this->_groupPrefix, $this->groups()); + } + $dir = $this->settings['path'] . $groups; - $this->_File->rewind(); - $success = $this->_File->ftruncate(0) && $this->_File->fwrite($contents) && $this->_File->fflush(); + if (!is_dir($dir)) { + mkdir($dir, 0775, true); + } + $path = new SplFileInfo($dir . $key); - if ($this->settings['lock']) { - $this->_File->flock(LOCK_UN); - } + if (!$createKey && !$path->isFile()) { + return false; + } + if ( + empty($this->_File) || + $this->_File->getBaseName() !== $key || + $this->_File->valid() === false + ) { + $exists = file_exists($path->getPathname()); + try { + $this->_File = $path->openFile('c+'); + } catch (Exception $e) { + trigger_error($e->getMessage(), E_USER_WARNING); + return false; + } + unset($path); - return $success; - } + if (!$exists && !chmod($this->_File->getPathname(), (int)$this->settings['mask'])) { + trigger_error(__d( + 'cake_dev', 'Could not apply permission mask "%s" on cache file "%s"', + [$this->_File->getPathname(), $this->settings['mask']]), E_USER_WARNING); + } + } + return true; + } + + /** + * Not implemented + * + * @param string $key The key to decrement + * @param int $offset The number to offset + * @return void + * @throws CacheException + */ + public function decrement($key, $offset = 1) + { + throw new CacheException(__d('cake_dev', 'Files cannot be atomically decremented.')); + } + + /** + * Not implemented + * + * @param string $key The key to decrement + * @param int $offset The number to offset + * @return void + * @throws CacheException + */ + public function increment($key, $offset = 1) + { + throw new CacheException(__d('cake_dev', 'Files cannot be atomically incremented.')); + } + + /** + * Generates a safe key for use with cache engine storage engines. + * + * @param string $key the key passed over + * @return mixed string $key or false + */ + public function key($key) + { + if (empty($key)) { + return false; + } -/** - * Read a key from the cache - * - * @param string $key Identifier for the data - * @return mixed The cached data, or false if the data doesn't exist, has expired, or if there was an error fetching it - */ - public function read($key) { - if (!$this->_init || $this->_setKey($key) === false) { - return false; - } - - if ($this->settings['lock']) { - $this->_File->flock(LOCK_SH); - } - - $this->_File->rewind(); - $time = time(); - $cachetime = (int)$this->_File->current(); - - if ($cachetime !== false && ($cachetime < $time || ($time + $this->settings['duration']) < $cachetime)) { - if ($this->settings['lock']) { - $this->_File->flock(LOCK_UN); - } - return false; - } - - $data = ''; - $this->_File->next(); - while ($this->_File->valid()) { - $data .= $this->_File->current(); - $this->_File->next(); - } - - if ($this->settings['lock']) { - $this->_File->flock(LOCK_UN); - } - - $data = trim($data); - - if ($data !== '' && !empty($this->settings['serialize'])) { - if ($this->settings['isWindows']) { - $data = str_replace('\\\\\\\\', '\\', $data); - } - $data = unserialize((string)$data); - } - return $data; - } + $key = Inflector::underscore(str_replace([DS, '/', '.', '<', '>', '?', ':', '|', '*', '"'], '_', strval($key))); + return $key; + } + + /** + * Recursively deletes all files under any directory named as $group + * + * @param string $group The group to clear. + * @return bool success + */ + public function clearGroup($group) + { + $this->_File = null; + $directoryIterator = new RecursiveDirectoryIterator($this->settings['path']); + $contents = new RecursiveIteratorIterator($directoryIterator, RecursiveIteratorIterator::CHILD_FIRST); + foreach ($contents as $object) { + $containsGroup = strpos($object->getPathName(), DS . $group . DS) !== false; + $hasPrefix = true; + if (strlen($this->settings['prefix']) !== 0) { + $hasPrefix = strpos($object->getBaseName(), $this->settings['prefix']) === 0; + } + if ($object->isFile() && $containsGroup && $hasPrefix) { + $path = $object->getPathName(); + $object = null; + //@codingStandardsIgnoreStart + @unlink($path); + //@codingStandardsIgnoreEnd + } + } + return true; + } + + /** + * Write data for key into cache if it doesn't exist already. + * If it already exists, it fails and returns false. + * + * @param string $key Identifier for the data. + * @param mixed $value Data to be cached. + * @param int $duration How long to cache the data, in seconds. + * @return bool True if the data was successfully cached, false on failure. + */ + public function add($key, $value, $duration) + { + $cachedValue = $this->read($key); + if ($cachedValue === false) { + return $this->write($key, $value, $duration); + } + return false; + } + + /** + * Read a key from the cache + * + * @param string $key Identifier for the data + * @return mixed The cached data, or false if the data doesn't exist, has expired, or if there was an error fetching it + */ + public function read($key) + { + if (!$this->_init || $this->_setKey($key) === false) { + return false; + } -/** - * Delete a key from the cache - * - * @param string $key Identifier for the data - * @return bool True if the value was successfully deleted, false if it didn't exist or couldn't be removed - */ - public function delete($key) { - if ($this->_setKey($key) === false || !$this->_init) { - return false; - } - $path = $this->_File->getRealPath(); - $this->_File = null; - - //@codingStandardsIgnoreStart - return @unlink($path); - //@codingStandardsIgnoreEnd - } + if ($this->settings['lock']) { + $this->_File->flock(LOCK_SH); + } -/** - * Delete all values from the cache - * - * @param bool $check Optional - only delete expired cache items - * @return bool True if the cache was successfully cleared, false otherwise - */ - public function clear($check) { - if (!$this->_init) { - return false; - } - $this->_File = null; - - $threshold = $now = false; - if ($check) { - $now = time(); - $threshold = $now - $this->settings['duration']; - } - - $this->_clearDirectory($this->settings['path'], $now, $threshold); - - $directory = new RecursiveDirectoryIterator($this->settings['path']); - $contents = new RecursiveIteratorIterator($directory, RecursiveIteratorIterator::SELF_FIRST); - $cleared = array(); - foreach ($contents as $path) { - if ($path->isFile()) { - continue; - } - - $path = $path->getRealPath() . DS; - if (!in_array($path, $cleared)) { - $this->_clearDirectory($path, $now, $threshold); - $cleared[] = $path; - } - } - return true; - } + $this->_File->rewind(); + $time = time(); + $cachetime = (int)$this->_File->current(); -/** - * Used to clear a directory of matching files. - * - * @param string $path The path to search. - * @param int $now The current timestamp - * @param int $threshold Any file not modified after this value will be deleted. - * @return void - */ - protected function _clearDirectory($path, $now, $threshold) { - $prefixLength = strlen($this->settings['prefix']); - - if (!is_dir($path)) { - return; - } - - $dir = dir($path); - if ($dir === false) { - return; - } - - while (($entry = $dir->read()) !== false) { - if (substr($entry, 0, $prefixLength) !== $this->settings['prefix']) { - continue; - } - - try { - $file = new SplFileObject($path . $entry, 'r'); - } catch (Exception $e) { - continue; - } - - if ($threshold) { - $mtime = $file->getMTime(); - - if ($mtime > $threshold) { - continue; - } - $expires = (int)$file->current(); - - if ($expires > $now) { - continue; - } - } - if ($file->isFile()) { - $filePath = $file->getRealPath(); - $file = null; - - //@codingStandardsIgnoreStart - @unlink($filePath); - //@codingStandardsIgnoreEnd - } - } - } + if ($cachetime !== false && ($cachetime < $time || ($time + $this->settings['duration']) < $cachetime)) { + if ($this->settings['lock']) { + $this->_File->flock(LOCK_UN); + } + return false; + } -/** - * Not implemented - * - * @param string $key The key to decrement - * @param int $offset The number to offset - * @return void - * @throws CacheException - */ - public function decrement($key, $offset = 1) { - throw new CacheException(__d('cake_dev', 'Files cannot be atomically decremented.')); - } + $data = ''; + $this->_File->next(); + while ($this->_File->valid()) { + $data .= $this->_File->current(); + $this->_File->next(); + } -/** - * Not implemented - * - * @param string $key The key to decrement - * @param int $offset The number to offset - * @return void - * @throws CacheException - */ - public function increment($key, $offset = 1) { - throw new CacheException(__d('cake_dev', 'Files cannot be atomically incremented.')); - } + if ($this->settings['lock']) { + $this->_File->flock(LOCK_UN); + } -/** - * Sets the current cache key this class is managing, and creates a writable SplFileObject - * for the cache file the key is referring to. - * - * @param string $key The key - * @param bool $createKey Whether the key should be created if it doesn't exists, or not - * @return bool true if the cache key could be set, false otherwise - */ - protected function _setKey($key, $createKey = false) { - $groups = null; - if (!empty($this->_groupPrefix)) { - $groups = vsprintf($this->_groupPrefix, $this->groups()); - } - $dir = $this->settings['path'] . $groups; - - if (!is_dir($dir)) { - mkdir($dir, 0775, true); - } - $path = new SplFileInfo($dir . $key); - - if (!$createKey && !$path->isFile()) { - return false; - } - if ( - empty($this->_File) || - $this->_File->getBaseName() !== $key || - $this->_File->valid() === false - ) { - $exists = file_exists($path->getPathname()); - try { - $this->_File = $path->openFile('c+'); - } catch (Exception $e) { - trigger_error($e->getMessage(), E_USER_WARNING); - return false; - } - unset($path); - - if (!$exists && !chmod($this->_File->getPathname(), (int)$this->settings['mask'])) { - trigger_error(__d( - 'cake_dev', 'Could not apply permission mask "%s" on cache file "%s"', - array($this->_File->getPathname(), $this->settings['mask'])), E_USER_WARNING); - } - } - return true; - } + $data = trim($data); -/** - * Determine is cache directory is writable - * - * @return bool - */ - protected function _active() { - $dir = new SplFileInfo($this->settings['path']); - $path = $dir->getPathname(); - if (!is_dir($path)) { - if(!mkdir($path, 0775, true)) { - exit(file_get_contents(ROOT.'/lib/Cake/View/Errors/permissions_errors.ctp')); - return false; + if ($data !== '' && !empty($this->settings['serialize'])) { + if ($this->settings['isWindows']) { + $data = str_replace('\\\\\\\\', '\\', $data); } + $data = unserialize((string)$data); + } + return $data; + } + + /** + * Write data for key into cache + * + * @param string $key Identifier for the data + * @param mixed $data Data to be cached + * @param int $duration How long to cache the data, in seconds + * @return bool True if the data was successfully cached, false on failure + */ + public function write($key, $data, $duration) + { + if (!$this->_init) { + return false; } - if ($this->_init && !($dir->isDir() && $dir->isWritable())) { - $this->_init = false; - exit(file_get_contents(ROOT.'/lib/Cake/View/Errors/permissions_errors.ctp')); - return false; - } - return true; - } -/** - * Generates a safe key for use with cache engine storage engines. - * - * @param string $key the key passed over - * @return mixed string $key or false - */ - public function key($key) { - if (empty($key)) { - return false; - } + if ($this->_setKey($key, true) === false) { + return false; + } - $key = Inflector::underscore(str_replace(array(DS, '/', '.', '<', '>', '?', ':', '|', '*', '"'), '_', strval($key))); - return $key; - } + $lineBreak = "\n"; -/** - * Recursively deletes all files under any directory named as $group - * - * @param string $group The group to clear. - * @return bool success - */ - public function clearGroup($group) { - $this->_File = null; - $directoryIterator = new RecursiveDirectoryIterator($this->settings['path']); - $contents = new RecursiveIteratorIterator($directoryIterator, RecursiveIteratorIterator::CHILD_FIRST); - foreach ($contents as $object) { - $containsGroup = strpos($object->getPathName(), DS . $group . DS) !== false; - $hasPrefix = true; - if (strlen($this->settings['prefix']) !== 0) { - $hasPrefix = strpos($object->getBaseName(), $this->settings['prefix']) === 0; - } - if ($object->isFile() && $containsGroup && $hasPrefix) { - $path = $object->getPathName(); - $object = null; - //@codingStandardsIgnoreStart - @unlink($path); - //@codingStandardsIgnoreEnd - } - } - return true; - } + if ($this->settings['isWindows']) { + $lineBreak = "\r\n"; + } -/** - * Write data for key into cache if it doesn't exist already. - * If it already exists, it fails and returns false. - * - * @param string $key Identifier for the data. - * @param mixed $value Data to be cached. - * @param int $duration How long to cache the data, in seconds. - * @return bool True if the data was successfully cached, false on failure. - */ - public function add($key, $value, $duration) { - $cachedValue = $this->read($key); - if ($cachedValue === false) { - return $this->write($key, $value, $duration); - } - return false; - } + if (!empty($this->settings['serialize'])) { + if ($this->settings['isWindows']) { + $data = str_replace('\\', '\\\\\\\\', serialize($data)); + } else { + $data = serialize($data); + } + } + + $expires = time() + $duration; + $contents = implode([$expires, $lineBreak, $data, $lineBreak]); + + if ($this->settings['lock']) { + $this->_File->flock(LOCK_EX); + } + + $this->_File->rewind(); + $success = $this->_File->ftruncate(0) && $this->_File->fwrite($contents) && $this->_File->fflush(); + + if ($this->settings['lock']) { + $this->_File->flock(LOCK_UN); + } + + return $success; + } } diff --git a/lib/Cake/Cache/Engine/MemcacheEngine.php b/lib/Cake/Cache/Engine/MemcacheEngine.php index d70c1c56..b70784c4 100755 --- a/lib/Cake/Cache/Engine/MemcacheEngine.php +++ b/lib/Cake/Cache/Engine/MemcacheEngine.php @@ -24,289 +24,300 @@ * @package Cake.Cache.Engine * @deprecated 3.0.0 You should use the Memcached adapter instead. */ -class MemcacheEngine extends CacheEngine { +class MemcacheEngine extends CacheEngine +{ -/** - * Contains the compiled group names - * (prefixed with the global configuration prefix) - * - * @var array - */ - protected $_compiledGroupNames = array(); + /** + * Settings + * + * - servers = string or array of memcache servers, default => 127.0.0.1. If an + * array MemcacheEngine will use them as a pool. + * - compress = boolean, default => false + * + * @var array + */ + public $settings = []; + /** + * Contains the compiled group names + * (prefixed with the global configuration prefix) + * + * @var array + */ + protected $_compiledGroupNames = []; + /** + * Memcache wrapper. + * + * @var Memcache + */ + protected $_Memcache = null; -/** - * Memcache wrapper. - * - * @var Memcache - */ - protected $_Memcache = null; + /** + * Initialize the Cache Engine + * + * Called automatically by the cache frontend + * To reinitialize the settings call Cache::engine('EngineName', [optional] settings = array()); + * + * @param array $settings array of setting for the engine + * @return bool True if the engine has been successfully initialized, false if not + */ + public function init($settings = []) + { + if (!class_exists('Memcache')) { + return false; + } + if (!isset($settings['prefix'])) { + $settings['prefix'] = Inflector::slug(APP_DIR) . '_'; + } + $settings += [ + 'engine' => 'Memcache', + 'servers' => ['127.0.0.1'], + 'compress' => false, + 'persistent' => true + ]; + parent::init($settings); -/** - * Settings - * - * - servers = string or array of memcache servers, default => 127.0.0.1. If an - * array MemcacheEngine will use them as a pool. - * - compress = boolean, default => false - * - * @var array - */ - public $settings = array(); + if ($this->settings['compress']) { + $this->settings['compress'] = MEMCACHE_COMPRESSED; + } + if (is_string($this->settings['servers'])) { + $this->settings['servers'] = [$this->settings['servers']]; + } + if (!isset($this->_Memcache)) { + $return = false; + $this->_Memcache = new Memcache(); + foreach ($this->settings['servers'] as $server) { + list($host, $port) = $this->_parseServerString($server); + if ($this->_Memcache->addServer($host, $port, $this->settings['persistent'])) { + $return = true; + } + } + return $return; + } + return true; + } -/** - * Initialize the Cache Engine - * - * Called automatically by the cache frontend - * To reinitialize the settings call Cache::engine('EngineName', [optional] settings = array()); - * - * @param array $settings array of setting for the engine - * @return bool True if the engine has been successfully initialized, false if not - */ - public function init($settings = array()) { - if (!class_exists('Memcache')) { - return false; - } - if (!isset($settings['prefix'])) { - $settings['prefix'] = Inflector::slug(APP_DIR) . '_'; - } - $settings += array( - 'engine' => 'Memcache', - 'servers' => array('127.0.0.1'), - 'compress' => false, - 'persistent' => true - ); - parent::init($settings); + /** + * Parses the server address into the host/port. Handles both IPv6 and IPv4 + * addresses and Unix sockets + * + * @param string $server The server address string. + * @return array Array containing host, port + */ + protected function _parseServerString($server) + { + if (strpos($server, 'unix://') === 0) { + return [$server, 0]; + } + if (substr($server, 0, 1) === '[') { + $position = strpos($server, ']:'); + if ($position !== false) { + $position++; + } + } else { + $position = strpos($server, ':'); + } + $port = 11211; + $host = $server; + if ($position !== false) { + $host = substr($server, 0, $position); + $port = substr($server, $position + 1); + } + return [$host, $port]; + } - if ($this->settings['compress']) { - $this->settings['compress'] = MEMCACHE_COMPRESSED; - } - if (is_string($this->settings['servers'])) { - $this->settings['servers'] = array($this->settings['servers']); - } - if (!isset($this->_Memcache)) { - $return = false; - $this->_Memcache = new Memcache(); - foreach ($this->settings['servers'] as $server) { - list($host, $port) = $this->_parseServerString($server); - if ($this->_Memcache->addServer($host, $port, $this->settings['persistent'])) { - $return = true; - } - } - return $return; - } - return true; - } + /** + * Write data for key into cache. When using memcache as your cache engine + * remember that the Memcache pecl extension does not support cache expiry times greater + * than 30 days in the future. Any duration greater than 30 days will be treated as never expiring. + * + * @param string $key Identifier for the data + * @param mixed $value Data to be cached + * @param int $duration How long to cache the data, in seconds + * @return bool True if the data was successfully cached, false on failure + * @see http://php.net/manual/en/memcache.set.php + */ + public function write($key, $value, $duration) + { + if ($duration > 30 * DAY) { + $duration = 0; + } + return $this->_Memcache->set($key, $value, $this->settings['compress'], $duration); + } -/** - * Parses the server address into the host/port. Handles both IPv6 and IPv4 - * addresses and Unix sockets - * - * @param string $server The server address string. - * @return array Array containing host, port - */ - protected function _parseServerString($server) { - if (strpos($server, 'unix://') === 0) { - return array($server, 0); - } - if (substr($server, 0, 1) === '[') { - $position = strpos($server, ']:'); - if ($position !== false) { - $position++; - } - } else { - $position = strpos($server, ':'); - } - $port = 11211; - $host = $server; - if ($position !== false) { - $host = substr($server, 0, $position); - $port = substr($server, $position + 1); - } - return array($host, $port); - } + /** + * Read a key from the cache + * + * @param string $key Identifier for the data + * @return mixed The cached data, or false if the data doesn't exist, has expired, or if there was an error fetching it + */ + public function read($key) + { + return $this->_Memcache->get($key); + } -/** - * Write data for key into cache. When using memcache as your cache engine - * remember that the Memcache pecl extension does not support cache expiry times greater - * than 30 days in the future. Any duration greater than 30 days will be treated as never expiring. - * - * @param string $key Identifier for the data - * @param mixed $value Data to be cached - * @param int $duration How long to cache the data, in seconds - * @return bool True if the data was successfully cached, false on failure - * @see http://php.net/manual/en/memcache.set.php - */ - public function write($key, $value, $duration) { - if ($duration > 30 * DAY) { - $duration = 0; - } - return $this->_Memcache->set($key, $value, $this->settings['compress'], $duration); - } + /** + * Increments the value of an integer cached key + * + * @param string $key Identifier for the data + * @param int $offset How much to increment + * @return New incremented value, false otherwise + * @throws CacheException when you try to increment with compress = true + */ + public function increment($key, $offset = 1) + { + if ($this->settings['compress']) { + throw new CacheException( + __d('cake_dev', 'Method %s not implemented for compressed cache in %s', 'increment()', __CLASS__) + ); + } + return $this->_Memcache->increment($key, $offset); + } -/** - * Read a key from the cache - * - * @param string $key Identifier for the data - * @return mixed The cached data, or false if the data doesn't exist, has expired, or if there was an error fetching it - */ - public function read($key) { - return $this->_Memcache->get($key); - } + /** + * Decrements the value of an integer cached key + * + * @param string $key Identifier for the data + * @param int $offset How much to subtract + * @return New decremented value, false otherwise + * @throws CacheException when you try to decrement with compress = true + */ + public function decrement($key, $offset = 1) + { + if ($this->settings['compress']) { + throw new CacheException( + __d('cake_dev', 'Method %s not implemented for compressed cache in %s', 'decrement()', __CLASS__) + ); + } + return $this->_Memcache->decrement($key, $offset); + } -/** - * Increments the value of an integer cached key - * - * @param string $key Identifier for the data - * @param int $offset How much to increment - * @return New incremented value, false otherwise - * @throws CacheException when you try to increment with compress = true - */ - public function increment($key, $offset = 1) { - if ($this->settings['compress']) { - throw new CacheException( - __d('cake_dev', 'Method %s not implemented for compressed cache in %s', 'increment()', __CLASS__) - ); - } - return $this->_Memcache->increment($key, $offset); - } - -/** - * Decrements the value of an integer cached key - * - * @param string $key Identifier for the data - * @param int $offset How much to subtract - * @return New decremented value, false otherwise - * @throws CacheException when you try to decrement with compress = true - */ - public function decrement($key, $offset = 1) { - if ($this->settings['compress']) { - throw new CacheException( - __d('cake_dev', 'Method %s not implemented for compressed cache in %s', 'decrement()', __CLASS__) - ); - } - return $this->_Memcache->decrement($key, $offset); - } - -/** - * Delete a key from the cache - * - * @param string $key Identifier for the data - * @return bool True if the value was successfully deleted, false if it didn't exist or couldn't be removed - */ - public function delete($key) { - return $this->_Memcache->delete($key); - } + /** + * Delete a key from the cache + * + * @param string $key Identifier for the data + * @return bool True if the value was successfully deleted, false if it didn't exist or couldn't be removed + */ + public function delete($key) + { + return $this->_Memcache->delete($key); + } -/** - * Delete all keys from the cache - * - * @param bool $check If true no deletes will occur and instead CakePHP will rely - * on key TTL values. - * @return bool True if the cache was successfully cleared, false otherwise - */ - public function clear($check) { - if ($check) { - return true; - } - foreach ($this->_Memcache->getExtendedStats('slabs', 0) as $slabs) { - foreach (array_keys($slabs) as $slabId) { - if (!is_numeric($slabId)) { - continue; - } + /** + * Delete all keys from the cache + * + * @param bool $check If true no deletes will occur and instead CakePHP will rely + * on key TTL values. + * @return bool True if the cache was successfully cleared, false otherwise + */ + public function clear($check) + { + if ($check) { + return true; + } + foreach ($this->_Memcache->getExtendedStats('slabs', 0) as $slabs) { + foreach (array_keys($slabs) as $slabId) { + if (!is_numeric($slabId)) { + continue; + } - foreach ($this->_Memcache->getExtendedStats('cachedump', $slabId, 0) as $stats) { - if (!is_array($stats)) { - continue; - } - foreach (array_keys($stats) as $key) { - if (strpos($key, $this->settings['prefix']) === 0) { - $this->_Memcache->delete($key); - } - } - } - } - } - return true; - } + foreach ($this->_Memcache->getExtendedStats('cachedump', $slabId, 0) as $stats) { + if (!is_array($stats)) { + continue; + } + foreach (array_keys($stats) as $key) { + if (strpos($key, $this->settings['prefix']) === 0) { + $this->_Memcache->delete($key); + } + } + } + } + } + return true; + } -/** - * Connects to a server in connection pool - * - * @param string $host host ip address or name - * @param int $port Server port - * @return bool True if memcache server was connected - */ - public function connect($host, $port = 11211) { - if ($this->_Memcache->getServerStatus($host, $port) === 0) { - if ($this->_Memcache->connect($host, $port)) { - return true; - } - return false; - } - return true; - } + /** + * Connects to a server in connection pool + * + * @param string $host host ip address or name + * @param int $port Server port + * @return bool True if memcache server was connected + */ + public function connect($host, $port = 11211) + { + if ($this->_Memcache->getServerStatus($host, $port) === 0) { + if ($this->_Memcache->connect($host, $port)) { + return true; + } + return false; + } + return true; + } -/** - * Returns the `group value` for each of the configured groups - * If the group initial value was not found, then it initializes - * the group accordingly. - * - * @return array - */ - public function groups() { - if (empty($this->_compiledGroupNames)) { - foreach ($this->settings['groups'] as $group) { - $this->_compiledGroupNames[] = $this->settings['prefix'] . $group; - } - } + /** + * Returns the `group value` for each of the configured groups + * If the group initial value was not found, then it initializes + * the group accordingly. + * + * @return array + */ + public function groups() + { + if (empty($this->_compiledGroupNames)) { + foreach ($this->settings['groups'] as $group) { + $this->_compiledGroupNames[] = $this->settings['prefix'] . $group; + } + } - $groups = $this->_Memcache->get($this->_compiledGroupNames); - if (count($groups) !== count($this->settings['groups'])) { - foreach ($this->_compiledGroupNames as $group) { - if (!isset($groups[$group])) { - $this->_Memcache->set($group, 1, false, 0); - $groups[$group] = 1; - } - } - ksort($groups); - } + $groups = $this->_Memcache->get($this->_compiledGroupNames); + if (count($groups) !== count($this->settings['groups'])) { + foreach ($this->_compiledGroupNames as $group) { + if (!isset($groups[$group])) { + $this->_Memcache->set($group, 1, false, 0); + $groups[$group] = 1; + } + } + ksort($groups); + } - $result = array(); - $groups = array_values($groups); - foreach ($this->settings['groups'] as $i => $group) { - $result[] = $group . $groups[$i]; - } + $result = []; + $groups = array_values($groups); + foreach ($this->settings['groups'] as $i => $group) { + $result[] = $group . $groups[$i]; + } - return $result; - } + return $result; + } -/** - * Increments the group value to simulate deletion of all keys under a group - * old values will remain in storage until they expire. - * - * @param string $group The group to clear. - * @return bool success - */ - public function clearGroup($group) { - return (bool)$this->_Memcache->increment($this->settings['prefix'] . $group); - } + /** + * Increments the group value to simulate deletion of all keys under a group + * old values will remain in storage until they expire. + * + * @param string $group The group to clear. + * @return bool success + */ + public function clearGroup($group) + { + return (bool)$this->_Memcache->increment($this->settings['prefix'] . $group); + } -/** - * Write data for key into cache if it doesn't exist already. When using memcached as your cache engine - * remember that the Memcached PECL extension does not support cache expiry times greater - * than 30 days in the future. Any duration greater than 30 days will be treated as never expiring. - * If it already exists, it fails and returns false. - * - * @param string $key Identifier for the data. - * @param mixed $value Data to be cached. - * @param int $duration How long to cache the data, in seconds. - * @return bool True if the data was successfully cached, false on failure. - * @link http://php.net/manual/en/memcache.add.php - */ - public function add($key, $value, $duration) { - if ($duration > 30 * DAY) { - $duration = 0; - } + /** + * Write data for key into cache if it doesn't exist already. When using memcached as your cache engine + * remember that the Memcached PECL extension does not support cache expiry times greater + * than 30 days in the future. Any duration greater than 30 days will be treated as never expiring. + * If it already exists, it fails and returns false. + * + * @param string $key Identifier for the data. + * @param mixed $value Data to be cached. + * @param int $duration How long to cache the data, in seconds. + * @return bool True if the data was successfully cached, false on failure. + * @link http://php.net/manual/en/memcache.add.php + */ + public function add($key, $value, $duration) + { + if ($duration > 30 * DAY) { + $duration = 0; + } - return $this->_Memcache->add($key, $value, $this->settings['compress'], $duration); - } + return $this->_Memcache->add($key, $value, $this->settings['compress'], $duration); + } } diff --git a/lib/Cake/Cache/Engine/MemcachedEngine.php b/lib/Cake/Cache/Engine/MemcachedEngine.php index 655eb5d1..280a4b29 100755 --- a/lib/Cake/Cache/Engine/MemcachedEngine.php +++ b/lib/Cake/Cache/Engine/MemcachedEngine.php @@ -27,339 +27,350 @@ * * @package Cake.Cache.Engine */ -class MemcachedEngine extends CacheEngine { - -/** - * memcached wrapper. - * - * @var Memcache - */ - protected $_Memcached = null; - -/** - * Settings - * - * - servers = string or array of memcached servers, default => 127.0.0.1. If an - * array MemcacheEngine will use them as a pool. - * - compress = boolean, default => false - * - persistent = string The name of the persistent connection. All configurations using - * the same persistent value will share a single underlying connection. - * - serialize = string, default => php. The serializer engine used to serialize data. - * Available engines are php, igbinary and json. Beside php, the memcached extension - * must be compiled with the appropriate serializer support. - * - options - Additional options for the memcached client. Should be an array of option => value. - * Use the Memcached::OPT_* constants as keys. - * - * @var array - */ - public $settings = array(); - -/** - * List of available serializer engines - * - * Memcached must be compiled with json and igbinary support to use these engines - * - * @var array - */ - protected $_serializers = array( - 'igbinary' => Memcached::SERIALIZER_IGBINARY, - 'json' => Memcached::SERIALIZER_JSON, - 'php' => Memcached::SERIALIZER_PHP - ); - -/** - * Initialize the Cache Engine - * - * Called automatically by the cache frontend - * To reinitialize the settings call Cache::engine('EngineName', [optional] settings = array()); - * - * @param array $settings array of setting for the engine - * @return bool True if the engine has been successfully initialized, false if not - * @throws CacheException when you try use authentication without Memcached compiled with SASL support - */ - public function init($settings = array()) { - if (!class_exists('Memcached')) { - return false; - } - if (!isset($settings['prefix'])) { - $settings['prefix'] = Inflector::slug(APP_DIR) . '_'; - } - - if (defined('Memcached::HAVE_MSGPACK') && Memcached::HAVE_MSGPACK) { - $this->_serializers['msgpack'] = Memcached::SERIALIZER_MSGPACK; - } - - $settings += array( - 'engine' => 'Memcached', - 'servers' => array('127.0.0.1'), - 'compress' => false, - 'persistent' => false, - 'login' => null, - 'password' => null, - 'serialize' => 'php', - 'options' => array() - ); - parent::init($settings); - - if (!is_array($this->settings['servers'])) { - $this->settings['servers'] = array($this->settings['servers']); - } - - if (isset($this->_Memcached)) { - return true; - } - - if (!$this->settings['persistent']) { - $this->_Memcached = new Memcached(); - } else { - $this->_Memcached = new Memcached((string)$this->settings['persistent']); - } - $this->_setOptions(); - - if (count($this->_Memcached->getServerList())) { - return true; - } - - $servers = array(); - foreach ($this->settings['servers'] as $server) { - $servers[] = $this->_parseServerString($server); - } - - if (!$this->_Memcached->addServers($servers)) { - return false; - } - - if ($this->settings['login'] !== null && $this->settings['password'] !== null) { - if (!method_exists($this->_Memcached, 'setSaslAuthData')) { - throw new CacheException( - __d('cake_dev', 'Memcached extension is not build with SASL support') - ); - } - $this->_Memcached->setOption(Memcached::OPT_BINARY_PROTOCOL, true); - $this->_Memcached->setSaslAuthData($this->settings['login'], $this->settings['password']); - } - if (is_array($this->settings['options'])) { - foreach ($this->settings['options'] as $opt => $value) { - $this->_Memcached->setOption($opt, $value); - } - } - - return true; - } - -/** - * Settings the memcached instance - * - * @throws CacheException when the Memcached extension is not built with the desired serializer engine - * @return void - */ - protected function _setOptions() { - $this->_Memcached->setOption(Memcached::OPT_LIBKETAMA_COMPATIBLE, true); - - $serializer = strtolower($this->settings['serialize']); - if (!isset($this->_serializers[$serializer])) { - throw new CacheException( - __d('cake_dev', '%s is not a valid serializer engine for Memcached', $serializer) - ); - } - - if ($serializer !== 'php' && !constant('Memcached::HAVE_' . strtoupper($serializer))) { - throw new CacheException( - __d('cake_dev', 'Memcached extension is not compiled with %s support', $serializer) - ); - } - - $this->_Memcached->setOption(Memcached::OPT_SERIALIZER, $this->_serializers[$serializer]); - - // Check for Amazon ElastiCache instance - if (defined('Memcached::OPT_CLIENT_MODE') && defined('Memcached::DYNAMIC_CLIENT_MODE')) { - $this->_Memcached->setOption(Memcached::OPT_CLIENT_MODE, Memcached::DYNAMIC_CLIENT_MODE); - } - - $this->_Memcached->setOption(Memcached::OPT_COMPRESSION, (bool)$this->settings['compress']); - } - -/** - * Parses the server address into the host/port. Handles both IPv6 and IPv4 - * addresses and Unix sockets - * - * @param string $server The server address string. - * @return array Array containing host, port - */ - protected function _parseServerString($server) { - $socketTransport = 'unix://'; - if (strpos($server, $socketTransport) === 0) { - return array(substr($server, strlen($socketTransport)), 0); - } - if (substr($server, 0, 1) === '[') { - $position = strpos($server, ']:'); - if ($position !== false) { - $position++; - } - } else { - $position = strpos($server, ':'); - } - $port = 11211; - $host = $server; - if ($position !== false) { - $host = substr($server, 0, $position); - $port = substr($server, $position + 1); - } - return array($host, (int)$port); - } - -/** - * Write data for key into cache. When using memcached as your cache engine - * remember that the Memcached PECL extension does not support cache expiry times greater - * than 30 days in the future. Any duration greater than 30 days will be treated as never expiring. - * - * @param string $key Identifier for the data - * @param mixed $value Data to be cached - * @param int $duration How long to cache the data, in seconds - * @return bool True if the data was successfully cached, false on failure - * @see http://php.net/manual/en/memcache.set.php - */ - public function write($key, $value, $duration) { - if ($duration > 30 * DAY) { - $duration = 0; - } - - return $this->_Memcached->set($key, $value, $duration); - } - -/** - * Read a key from the cache - * - * @param string $key Identifier for the data - * @return mixed The cached data, or false if the data doesn't exist, has expired, or if there was an error fetching it - */ - public function read($key) { - return $this->_Memcached->get($key); - } - -/** - * Increments the value of an integer cached key - * - * @param string $key Identifier for the data - * @param int $offset How much to increment - * @return New incremented value, false otherwise - * @throws CacheException when you try to increment with compress = true - */ - public function increment($key, $offset = 1) { - return $this->_Memcached->increment($key, $offset); - } - -/** - * Decrements the value of an integer cached key - * - * @param string $key Identifier for the data - * @param int $offset How much to subtract - * @return New decremented value, false otherwise - * @throws CacheException when you try to decrement with compress = true - */ - public function decrement($key, $offset = 1) { - return $this->_Memcached->decrement($key, $offset); - } - -/** - * Delete a key from the cache - * - * @param string $key Identifier for the data - * @return bool True if the value was successfully deleted, false if it didn't exist or couldn't be removed - */ - public function delete($key) { - return $this->_Memcached->delete($key); - } - -/** - * Delete all keys from the cache - * - * @param bool $check If true no deletes will occur and instead CakePHP will rely - * on key TTL values. - * @return bool True if the cache was successfully cleared, false otherwise. Will - * also return false if you are using a binary protocol. - */ - public function clear($check) { - if ($check) { - return true; - } - - $keys = $this->_Memcached->getAllKeys(); - if ($keys === false) { - return false; - } - - foreach ($keys as $key) { - if (strpos($key, $this->settings['prefix']) === 0) { - $this->_Memcached->delete($key); - } - } - - return true; - } - -/** - * Returns the `group value` for each of the configured groups - * If the group initial value was not found, then it initializes - * the group accordingly. - * - * @return array - */ - public function groups() { - if (empty($this->_compiledGroupNames)) { - foreach ($this->settings['groups'] as $group) { - $this->_compiledGroupNames[] = $this->settings['prefix'] . $group; - } - } - - $groups = $this->_Memcached->getMulti($this->_compiledGroupNames); - if (count($groups) !== count($this->settings['groups'])) { - foreach ($this->_compiledGroupNames as $group) { - if (!isset($groups[$group])) { - $this->_Memcached->set($group, 1, 0); - $groups[$group] = 1; - } - } - ksort($groups); - } - - $result = array(); - $groups = array_values($groups); - foreach ($this->settings['groups'] as $i => $group) { - $result[] = $group . $groups[$i]; - } - - return $result; - } - -/** - * Increments the group value to simulate deletion of all keys under a group - * old values will remain in storage until they expire. - * - * @param string $group The group to clear. - * @return bool success - */ - public function clearGroup($group) { - return (bool)$this->_Memcached->increment($this->settings['prefix'] . $group); - } - -/** - * Write data for key into cache if it doesn't exist already. When using memcached as your cache engine - * remember that the Memcached pecl extension does not support cache expiry times greater - * than 30 days in the future. Any duration greater than 30 days will be treated as never expiring. - * If it already exists, it fails and returns false. - * - * @param string $key Identifier for the data. - * @param mixed $value Data to be cached. - * @param int $duration How long to cache the data, in seconds. - * @return bool True if the data was successfully cached, false on failure. - * @link http://php.net/manual/en/memcached.add.php - */ - public function add($key, $value, $duration) { - if ($duration > 30 * DAY) { - $duration = 0; - } - - return $this->_Memcached->add($key, $value, $duration); - } +class MemcachedEngine extends CacheEngine +{ + + /** + * Settings + * + * - servers = string or array of memcached servers, default => 127.0.0.1. If an + * array MemcacheEngine will use them as a pool. + * - compress = boolean, default => false + * - persistent = string The name of the persistent connection. All configurations using + * the same persistent value will share a single underlying connection. + * - serialize = string, default => php. The serializer engine used to serialize data. + * Available engines are php, igbinary and json. Beside php, the memcached extension + * must be compiled with the appropriate serializer support. + * - options - Additional options for the memcached client. Should be an array of option => value. + * Use the Memcached::OPT_* constants as keys. + * + * @var array + */ + public $settings = []; + /** + * memcached wrapper. + * + * @var Memcache + */ + protected $_Memcached = null; + /** + * List of available serializer engines + * + * Memcached must be compiled with json and igbinary support to use these engines + * + * @var array + */ + protected $_serializers = [ + 'igbinary' => Memcached::SERIALIZER_IGBINARY, + 'json' => Memcached::SERIALIZER_JSON, + 'php' => Memcached::SERIALIZER_PHP + ]; + + /** + * Initialize the Cache Engine + * + * Called automatically by the cache frontend + * To reinitialize the settings call Cache::engine('EngineName', [optional] settings = array()); + * + * @param array $settings array of setting for the engine + * @return bool True if the engine has been successfully initialized, false if not + * @throws CacheException when you try use authentication without Memcached compiled with SASL support + */ + public function init($settings = []) + { + if (!class_exists('Memcached')) { + return false; + } + if (!isset($settings['prefix'])) { + $settings['prefix'] = Inflector::slug(APP_DIR) . '_'; + } + + if (defined('Memcached::HAVE_MSGPACK') && Memcached::HAVE_MSGPACK) { + $this->_serializers['msgpack'] = Memcached::SERIALIZER_MSGPACK; + } + + $settings += [ + 'engine' => 'Memcached', + 'servers' => ['127.0.0.1'], + 'compress' => false, + 'persistent' => false, + 'login' => null, + 'password' => null, + 'serialize' => 'php', + 'options' => [] + ]; + parent::init($settings); + + if (!is_array($this->settings['servers'])) { + $this->settings['servers'] = [$this->settings['servers']]; + } + + if (isset($this->_Memcached)) { + return true; + } + + if (!$this->settings['persistent']) { + $this->_Memcached = new Memcached(); + } else { + $this->_Memcached = new Memcached((string)$this->settings['persistent']); + } + $this->_setOptions(); + + if (count($this->_Memcached->getServerList())) { + return true; + } + + $servers = []; + foreach ($this->settings['servers'] as $server) { + $servers[] = $this->_parseServerString($server); + } + + if (!$this->_Memcached->addServers($servers)) { + return false; + } + + if ($this->settings['login'] !== null && $this->settings['password'] !== null) { + if (!method_exists($this->_Memcached, 'setSaslAuthData')) { + throw new CacheException( + __d('cake_dev', 'Memcached extension is not build with SASL support') + ); + } + $this->_Memcached->setOption(Memcached::OPT_BINARY_PROTOCOL, true); + $this->_Memcached->setSaslAuthData($this->settings['login'], $this->settings['password']); + } + if (is_array($this->settings['options'])) { + foreach ($this->settings['options'] as $opt => $value) { + $this->_Memcached->setOption($opt, $value); + } + } + + return true; + } + + /** + * Settings the memcached instance + * + * @return void + * @throws CacheException when the Memcached extension is not built with the desired serializer engine + */ + protected function _setOptions() + { + $this->_Memcached->setOption(Memcached::OPT_LIBKETAMA_COMPATIBLE, true); + + $serializer = strtolower($this->settings['serialize']); + if (!isset($this->_serializers[$serializer])) { + throw new CacheException( + __d('cake_dev', '%s is not a valid serializer engine for Memcached', $serializer) + ); + } + + if ($serializer !== 'php' && !constant('Memcached::HAVE_' . strtoupper($serializer))) { + throw new CacheException( + __d('cake_dev', 'Memcached extension is not compiled with %s support', $serializer) + ); + } + + $this->_Memcached->setOption(Memcached::OPT_SERIALIZER, $this->_serializers[$serializer]); + + // Check for Amazon ElastiCache instance + if (defined('Memcached::OPT_CLIENT_MODE') && defined('Memcached::DYNAMIC_CLIENT_MODE')) { + $this->_Memcached->setOption(Memcached::OPT_CLIENT_MODE, Memcached::DYNAMIC_CLIENT_MODE); + } + + $this->_Memcached->setOption(Memcached::OPT_COMPRESSION, (bool)$this->settings['compress']); + } + + /** + * Parses the server address into the host/port. Handles both IPv6 and IPv4 + * addresses and Unix sockets + * + * @param string $server The server address string. + * @return array Array containing host, port + */ + protected function _parseServerString($server) + { + $socketTransport = 'unix://'; + if (strpos($server, $socketTransport) === 0) { + return [substr($server, strlen($socketTransport)), 0]; + } + if (substr($server, 0, 1) === '[') { + $position = strpos($server, ']:'); + if ($position !== false) { + $position++; + } + } else { + $position = strpos($server, ':'); + } + $port = 11211; + $host = $server; + if ($position !== false) { + $host = substr($server, 0, $position); + $port = substr($server, $position + 1); + } + return [$host, (int)$port]; + } + + /** + * Write data for key into cache. When using memcached as your cache engine + * remember that the Memcached PECL extension does not support cache expiry times greater + * than 30 days in the future. Any duration greater than 30 days will be treated as never expiring. + * + * @param string $key Identifier for the data + * @param mixed $value Data to be cached + * @param int $duration How long to cache the data, in seconds + * @return bool True if the data was successfully cached, false on failure + * @see http://php.net/manual/en/memcache.set.php + */ + public function write($key, $value, $duration) + { + if ($duration > 30 * DAY) { + $duration = 0; + } + + return $this->_Memcached->set($key, $value, $duration); + } + + /** + * Read a key from the cache + * + * @param string $key Identifier for the data + * @return mixed The cached data, or false if the data doesn't exist, has expired, or if there was an error fetching it + */ + public function read($key) + { + return $this->_Memcached->get($key); + } + + /** + * Increments the value of an integer cached key + * + * @param string $key Identifier for the data + * @param int $offset How much to increment + * @return New incremented value, false otherwise + * @throws CacheException when you try to increment with compress = true + */ + public function increment($key, $offset = 1) + { + return $this->_Memcached->increment($key, $offset); + } + + /** + * Decrements the value of an integer cached key + * + * @param string $key Identifier for the data + * @param int $offset How much to subtract + * @return New decremented value, false otherwise + * @throws CacheException when you try to decrement with compress = true + */ + public function decrement($key, $offset = 1) + { + return $this->_Memcached->decrement($key, $offset); + } + + /** + * Delete a key from the cache + * + * @param string $key Identifier for the data + * @return bool True if the value was successfully deleted, false if it didn't exist or couldn't be removed + */ + public function delete($key) + { + return $this->_Memcached->delete($key); + } + + /** + * Delete all keys from the cache + * + * @param bool $check If true no deletes will occur and instead CakePHP will rely + * on key TTL values. + * @return bool True if the cache was successfully cleared, false otherwise. Will + * also return false if you are using a binary protocol. + */ + public function clear($check) + { + if ($check) { + return true; + } + + $keys = $this->_Memcached->getAllKeys(); + if ($keys === false) { + return false; + } + + foreach ($keys as $key) { + if (strpos($key, $this->settings['prefix']) === 0) { + $this->_Memcached->delete($key); + } + } + + return true; + } + + /** + * Returns the `group value` for each of the configured groups + * If the group initial value was not found, then it initializes + * the group accordingly. + * + * @return array + */ + public function groups() + { + if (empty($this->_compiledGroupNames)) { + foreach ($this->settings['groups'] as $group) { + $this->_compiledGroupNames[] = $this->settings['prefix'] . $group; + } + } + + $groups = $this->_Memcached->getMulti($this->_compiledGroupNames); + if (count($groups) !== count($this->settings['groups'])) { + foreach ($this->_compiledGroupNames as $group) { + if (!isset($groups[$group])) { + $this->_Memcached->set($group, 1, 0); + $groups[$group] = 1; + } + } + ksort($groups); + } + + $result = []; + $groups = array_values($groups); + foreach ($this->settings['groups'] as $i => $group) { + $result[] = $group . $groups[$i]; + } + + return $result; + } + + /** + * Increments the group value to simulate deletion of all keys under a group + * old values will remain in storage until they expire. + * + * @param string $group The group to clear. + * @return bool success + */ + public function clearGroup($group) + { + return (bool)$this->_Memcached->increment($this->settings['prefix'] . $group); + } + + /** + * Write data for key into cache if it doesn't exist already. When using memcached as your cache engine + * remember that the Memcached pecl extension does not support cache expiry times greater + * than 30 days in the future. Any duration greater than 30 days will be treated as never expiring. + * If it already exists, it fails and returns false. + * + * @param string $key Identifier for the data. + * @param mixed $value Data to be cached. + * @param int $duration How long to cache the data, in seconds. + * @return bool True if the data was successfully cached, false on failure. + * @link http://php.net/manual/en/memcached.add.php + */ + public function add($key, $value, $duration) + { + if ($duration > 30 * DAY) { + $duration = 0; + } + + return $this->_Memcached->add($key, $value, $duration); + } } diff --git a/lib/Cake/Cache/Engine/RedisEngine.php b/lib/Cake/Cache/Engine/RedisEngine.php index 6dd65994..cace10f3 100755 --- a/lib/Cake/Cache/Engine/RedisEngine.php +++ b/lib/Cake/Cache/Engine/RedisEngine.php @@ -21,238 +21,250 @@ * * @package Cake.Cache.Engine */ -class RedisEngine extends CacheEngine { +class RedisEngine extends CacheEngine +{ -/** - * Redis wrapper. - * - * @var Redis - */ - protected $_Redis = null; + /** + * Settings + * + * - server = string URL or ip to the Redis server host + * - database = integer database number to use for connection + * - port = integer port number to the Redis server (default: 6379) + * - timeout = float timeout in seconds (default: 0) + * - persistent = boolean Connects to the Redis server with a persistent connection (default: true) + * - unix_socket = path to the unix socket file (default: false) + * + * @var array + */ + public $settings = []; + /** + * Redis wrapper. + * + * @var Redis + */ + protected $_Redis = null; -/** - * Settings - * - * - server = string URL or ip to the Redis server host - * - database = integer database number to use for connection - * - port = integer port number to the Redis server (default: 6379) - * - timeout = float timeout in seconds (default: 0) - * - persistent = boolean Connects to the Redis server with a persistent connection (default: true) - * - unix_socket = path to the unix socket file (default: false) - * - * @var array - */ - public $settings = array(); - -/** - * Initialize the Cache Engine - * - * Called automatically by the cache frontend - * To reinitialize the settings call Cache::engine('EngineName', [optional] settings = array()); - * - * @param array $settings array of setting for the engine - * @return bool True if the engine has been successfully initialized, false if not - */ - public function init($settings = array()) { - if (!class_exists('Redis')) { - return false; - } - parent::init(array_merge(array( - 'engine' => 'Redis', - 'prefix' => Inflector::slug(APP_DIR) . '_', - 'server' => '127.0.0.1', - 'database' => 0, - 'port' => 6379, - 'password' => false, - 'timeout' => 0, - 'persistent' => true, - 'unix_socket' => false - ), $settings) - ); + /** + * Initialize the Cache Engine + * + * Called automatically by the cache frontend + * To reinitialize the settings call Cache::engine('EngineName', [optional] settings = array()); + * + * @param array $settings array of setting for the engine + * @return bool True if the engine has been successfully initialized, false if not + */ + public function init($settings = []) + { + if (!class_exists('Redis')) { + return false; + } + parent::init(array_merge([ + 'engine' => 'Redis', + 'prefix' => Inflector::slug(APP_DIR) . '_', + 'server' => '127.0.0.1', + 'database' => 0, + 'port' => 6379, + 'password' => false, + 'timeout' => 0, + 'persistent' => true, + 'unix_socket' => false + ], $settings) + ); - return $this->_connect(); - } + return $this->_connect(); + } -/** - * Connects to a Redis server - * - * @return bool True if Redis server was connected - */ - protected function _connect() { - try { - $this->_Redis = new Redis(); - if (!empty($this->settings['unix_socket'])) { - $return = $this->_Redis->connect($this->settings['unix_socket']); - } elseif (empty($this->settings['persistent'])) { - $return = $this->_Redis->connect($this->settings['server'], $this->settings['port'], $this->settings['timeout']); - } else { - $persistentId = $this->settings['port'] . $this->settings['timeout'] . $this->settings['database']; - $return = $this->_Redis->pconnect($this->settings['server'], $this->settings['port'], $this->settings['timeout'], $persistentId); - } - } catch (RedisException $e) { - $return = false; - } - if (!$return) { - return false; - } - if ($this->settings['password'] && !$this->_Redis->auth($this->settings['password'])) { - return false; - } - return $this->_Redis->select($this->settings['database']); - } + /** + * Connects to a Redis server + * + * @return bool True if Redis server was connected + */ + protected function _connect() + { + try { + $this->_Redis = new Redis(); + if (!empty($this->settings['unix_socket'])) { + $return = $this->_Redis->connect($this->settings['unix_socket']); + } else if (empty($this->settings['persistent'])) { + $return = $this->_Redis->connect($this->settings['server'], $this->settings['port'], $this->settings['timeout']); + } else { + $persistentId = $this->settings['port'] . $this->settings['timeout'] . $this->settings['database']; + $return = $this->_Redis->pconnect($this->settings['server'], $this->settings['port'], $this->settings['timeout'], $persistentId); + } + } catch (RedisException $e) { + $return = false; + } + if (!$return) { + return false; + } + if ($this->settings['password'] && !$this->_Redis->auth($this->settings['password'])) { + return false; + } + return $this->_Redis->select($this->settings['database']); + } -/** - * Write data for key into cache. - * - * @param string $key Identifier for the data - * @param mixed $value Data to be cached - * @param int $duration How long to cache the data, in seconds - * @return bool True if the data was successfully cached, false on failure - */ - public function write($key, $value, $duration) { - if (!is_int($value)) { - $value = serialize($value); - } + /** + * Write data for key into cache. + * + * @param string $key Identifier for the data + * @param mixed $value Data to be cached + * @param int $duration How long to cache the data, in seconds + * @return bool True if the data was successfully cached, false on failure + */ + public function write($key, $value, $duration) + { + if (!is_int($value)) { + $value = serialize($value); + } - if (!$this->_Redis->isConnected()) { - $this->_connect(); - } + if (!$this->_Redis->isConnected()) { + $this->_connect(); + } - if ($duration === 0) { - return $this->_Redis->set($key, $value); - } + if ($duration === 0) { + return $this->_Redis->set($key, $value); + } - return $this->_Redis->setex($key, $duration, $value); - } + return $this->_Redis->setex($key, $duration, $value); + } -/** - * Read a key from the cache - * - * @param string $key Identifier for the data - * @return mixed The cached data, or false if the data doesn't exist, has expired, or if there was an error fetching it - */ - public function read($key) { - $value = $this->_Redis->get($key); - if (preg_match('/^[-]?\d+$/', $value)) { - return (int)$value; - } - if ($value !== false && is_string($value)) { - return unserialize($value); - } - return $value; - } + /** + * Read a key from the cache + * + * @param string $key Identifier for the data + * @return mixed The cached data, or false if the data doesn't exist, has expired, or if there was an error fetching it + */ + public function read($key) + { + $value = $this->_Redis->get($key); + if (preg_match('/^[-]?\d+$/', $value)) { + return (int)$value; + } + if ($value !== false && is_string($value)) { + return unserialize($value); + } + return $value; + } -/** - * Increments the value of an integer cached key - * - * @param string $key Identifier for the data - * @param int $offset How much to increment - * @return New incremented value, false otherwise - * @throws CacheException when you try to increment with compress = true - */ - public function increment($key, $offset = 1) { - return (int)$this->_Redis->incrBy($key, $offset); - } + /** + * Increments the value of an integer cached key + * + * @param string $key Identifier for the data + * @param int $offset How much to increment + * @return New incremented value, false otherwise + * @throws CacheException when you try to increment with compress = true + */ + public function increment($key, $offset = 1) + { + return (int)$this->_Redis->incrBy($key, $offset); + } -/** - * Decrements the value of an integer cached key - * - * @param string $key Identifier for the data - * @param int $offset How much to subtract - * @return New decremented value, false otherwise - * @throws CacheException when you try to decrement with compress = true - */ - public function decrement($key, $offset = 1) { - return (int)$this->_Redis->decrBy($key, $offset); - } + /** + * Decrements the value of an integer cached key + * + * @param string $key Identifier for the data + * @param int $offset How much to subtract + * @return New decremented value, false otherwise + * @throws CacheException when you try to decrement with compress = true + */ + public function decrement($key, $offset = 1) + { + return (int)$this->_Redis->decrBy($key, $offset); + } -/** - * Delete a key from the cache - * - * @param string $key Identifier for the data - * @return bool True if the value was successfully deleted, false if it didn't exist or couldn't be removed - */ - public function delete($key) { - return $this->_Redis->delete($key) > 0; - } + /** + * Delete a key from the cache + * + * @param string $key Identifier for the data + * @return bool True if the value was successfully deleted, false if it didn't exist or couldn't be removed + */ + public function delete($key) + { + return $this->_Redis->delete($key) > 0; + } -/** - * Delete all keys from the cache - * - * @param bool $check Whether or not expiration keys should be checked. If - * true, no keys will be removed as cache will rely on redis TTL's. - * @return bool True if the cache was successfully cleared, false otherwise - */ - public function clear($check) { - if ($check) { - return true; - } - $keys = $this->_Redis->getKeys($this->settings['prefix'] . '*'); - $this->_Redis->del($keys); + /** + * Delete all keys from the cache + * + * @param bool $check Whether or not expiration keys should be checked. If + * true, no keys will be removed as cache will rely on redis TTL's. + * @return bool True if the cache was successfully cleared, false otherwise + */ + public function clear($check) + { + if ($check) { + return true; + } + $keys = $this->_Redis->getKeys($this->settings['prefix'] . '*'); + $this->_Redis->del($keys); - return true; - } + return true; + } -/** - * Returns the `group value` for each of the configured groups - * If the group initial value was not found, then it initializes - * the group accordingly. - * - * @return array - */ - public function groups() { - $result = array(); - foreach ($this->settings['groups'] as $group) { - $value = $this->_Redis->get($this->settings['prefix'] . $group); - if (!$value) { - $value = 1; - $this->_Redis->set($this->settings['prefix'] . $group, $value); - } - $result[] = $group . $value; - } - return $result; - } + /** + * Returns the `group value` for each of the configured groups + * If the group initial value was not found, then it initializes + * the group accordingly. + * + * @return array + */ + public function groups() + { + $result = []; + foreach ($this->settings['groups'] as $group) { + $value = $this->_Redis->get($this->settings['prefix'] . $group); + if (!$value) { + $value = 1; + $this->_Redis->set($this->settings['prefix'] . $group, $value); + } + $result[] = $group . $value; + } + return $result; + } -/** - * Increments the group value to simulate deletion of all keys under a group - * old values will remain in storage until they expire. - * - * @param string $group The group name to clear. - * @return bool success - */ - public function clearGroup($group) { - return (bool)$this->_Redis->incr($this->settings['prefix'] . $group); - } + /** + * Increments the group value to simulate deletion of all keys under a group + * old values will remain in storage until they expire. + * + * @param string $group The group name to clear. + * @return bool success + */ + public function clearGroup($group) + { + return (bool)$this->_Redis->incr($this->settings['prefix'] . $group); + } -/** - * Disconnects from the redis server - */ - public function __destruct() { - if (empty($this->settings['persistent']) && $this->_Redis !== null) { - $this->_Redis->close(); - } - } + /** + * Disconnects from the redis server + */ + public function __destruct() + { + if (empty($this->settings['persistent']) && $this->_Redis !== null) { + $this->_Redis->close(); + } + } -/** - * Write data for key into cache if it doesn't exist already. - * If it already exists, it fails and returns false. - * - * @param string $key Identifier for the data. - * @param mixed $value Data to be cached. - * @param int $duration How long to cache the data, in seconds. - * @return bool True if the data was successfully cached, false on failure. - * @link https://github.com/phpredis/phpredis#setnx - */ - public function add($key, $value, $duration) { - if (!is_int($value)) { - $value = serialize($value); - } + /** + * Write data for key into cache if it doesn't exist already. + * If it already exists, it fails and returns false. + * + * @param string $key Identifier for the data. + * @param mixed $value Data to be cached. + * @param int $duration How long to cache the data, in seconds. + * @return bool True if the data was successfully cached, false on failure. + * @link https://github.com/phpredis/phpredis#setnx + */ + public function add($key, $value, $duration) + { + if (!is_int($value)) { + $value = serialize($value); + } - $result = $this->_Redis->setnx($key, $value); - // setnx() doesn't have an expiry option, so overwrite the key with one - if ($result) { - return $this->_Redis->setex($key, $duration, $value); - } - return false; - } + $result = $this->_Redis->setnx($key, $value); + // setnx() doesn't have an expiry option, so overwrite the key with one + if ($result) { + return $this->_Redis->setex($key, $duration, $value); + } + return false; + } } diff --git a/lib/Cake/Cache/Engine/WincacheEngine.php b/lib/Cake/Cache/Engine/WincacheEngine.php index dc834817..6084d455 100755 --- a/lib/Cake/Cache/Engine/WincacheEngine.php +++ b/lib/Cake/Cache/Engine/WincacheEngine.php @@ -23,185 +23,196 @@ * * @package Cake.Cache.Engine */ -class WincacheEngine extends CacheEngine { +class WincacheEngine extends CacheEngine +{ -/** - * Contains the compiled group names - * (prefixed with the global configuration prefix) - * - * @var array - */ - protected $_compiledGroupNames = array(); + /** + * Contains the compiled group names + * (prefixed with the global configuration prefix) + * + * @var array + */ + protected $_compiledGroupNames = []; -/** - * Initialize the Cache Engine - * - * Called automatically by the cache frontend - * To reinitialize the settings call Cache::engine('EngineName', [optional] settings = array()); - * - * @param array $settings array of setting for the engine - * @return bool True if the engine has been successfully initialized, false if not - * @see CacheEngine::__defaults - */ - public function init($settings = array()) { - if (!isset($settings['prefix'])) { - $settings['prefix'] = Inflector::slug(APP_DIR) . '_'; - } - $settings += array('engine' => 'Wincache'); - parent::init($settings); - return function_exists('wincache_ucache_info'); - } + /** + * Initialize the Cache Engine + * + * Called automatically by the cache frontend + * To reinitialize the settings call Cache::engine('EngineName', [optional] settings = array()); + * + * @param array $settings array of setting for the engine + * @return bool True if the engine has been successfully initialized, false if not + * @see CacheEngine::__defaults + */ + public function init($settings = []) + { + if (!isset($settings['prefix'])) { + $settings['prefix'] = Inflector::slug(APP_DIR) . '_'; + } + $settings += ['engine' => 'Wincache']; + parent::init($settings); + return function_exists('wincache_ucache_info'); + } -/** - * Write data for key into cache - * - * @param string $key Identifier for the data - * @param mixed $value Data to be cached - * @param int $duration How long to cache the data, in seconds - * @return bool True if the data was successfully cached, false on failure - */ - public function write($key, $value, $duration) { - $expires = time() + $duration; + /** + * Increments the value of an integer cached key + * + * @param string $key Identifier for the data + * @param int $offset How much to increment + * @return New incremented value, false otherwise + */ + public function increment($key, $offset = 1) + { + return wincache_ucache_inc($key, $offset); + } - $data = array( - $key . '_expires' => $expires, - $key => $value - ); - $result = wincache_ucache_set($data, null, $duration); - return empty($result); - } + /** + * Decrements the value of an integer cached key + * + * @param string $key Identifier for the data + * @param int $offset How much to subtract + * @return New decremented value, false otherwise + */ + public function decrement($key, $offset = 1) + { + return wincache_ucache_dec($key, $offset); + } -/** - * Read a key from the cache - * - * @param string $key Identifier for the data - * @return mixed The cached data, or false if the data doesn't exist, has expired, or if - * there was an error fetching it - */ - public function read($key) { - $time = time(); - $cachetime = (int)wincache_ucache_get($key . '_expires'); - if ($cachetime < $time || ($time + $this->settings['duration']) < $cachetime) { - return false; - } - return wincache_ucache_get($key); - } + /** + * Delete a key from the cache + * + * @param string $key Identifier for the data + * @return bool True if the value was successfully deleted, false if it didn't exist or couldn't be removed + */ + public function delete($key) + { + return wincache_ucache_delete($key); + } -/** - * Increments the value of an integer cached key - * - * @param string $key Identifier for the data - * @param int $offset How much to increment - * @return New incremented value, false otherwise - */ - public function increment($key, $offset = 1) { - return wincache_ucache_inc($key, $offset); - } + /** + * Delete all keys from the cache. This will clear every + * item in the cache matching the cache config prefix. + * + * @param bool $check If true, nothing will be cleared, as entries will + * naturally expire in wincache.. + * @return bool True Returns true. + */ + public function clear($check) + { + if ($check) { + return true; + } + $info = wincache_ucache_info(); + $cacheKeys = $info['ucache_entries']; + unset($info); + foreach ($cacheKeys as $key) { + if (strpos($key['key_name'], $this->settings['prefix']) === 0) { + wincache_ucache_delete($key['key_name']); + } + } + return true; + } -/** - * Decrements the value of an integer cached key - * - * @param string $key Identifier for the data - * @param int $offset How much to subtract - * @return New decremented value, false otherwise - */ - public function decrement($key, $offset = 1) { - return wincache_ucache_dec($key, $offset); - } + /** + * Returns the `group value` for each of the configured groups + * If the group initial value was not found, then it initializes + * the group accordingly. + * + * @return array + */ + public function groups() + { + if (empty($this->_compiledGroupNames)) { + foreach ($this->settings['groups'] as $group) { + $this->_compiledGroupNames[] = $this->settings['prefix'] . $group; + } + } -/** - * Delete a key from the cache - * - * @param string $key Identifier for the data - * @return bool True if the value was successfully deleted, false if it didn't exist or couldn't be removed - */ - public function delete($key) { - return wincache_ucache_delete($key); - } + $groups = wincache_ucache_get($this->_compiledGroupNames); + if (count($groups) !== count($this->settings['groups'])) { + foreach ($this->_compiledGroupNames as $group) { + if (!isset($groups[$group])) { + wincache_ucache_set($group, 1); + $groups[$group] = 1; + } + } + ksort($groups); + } -/** - * Delete all keys from the cache. This will clear every - * item in the cache matching the cache config prefix. - * - * @param bool $check If true, nothing will be cleared, as entries will - * naturally expire in wincache.. - * @return bool True Returns true. - */ - public function clear($check) { - if ($check) { - return true; - } - $info = wincache_ucache_info(); - $cacheKeys = $info['ucache_entries']; - unset($info); - foreach ($cacheKeys as $key) { - if (strpos($key['key_name'], $this->settings['prefix']) === 0) { - wincache_ucache_delete($key['key_name']); - } - } - return true; - } + $result = []; + $groups = array_values($groups); + foreach ($this->settings['groups'] as $i => $group) { + $result[] = $group . $groups[$i]; + } + return $result; + } -/** - * Returns the `group value` for each of the configured groups - * If the group initial value was not found, then it initializes - * the group accordingly. - * - * @return array - */ - public function groups() { - if (empty($this->_compiledGroupNames)) { - foreach ($this->settings['groups'] as $group) { - $this->_compiledGroupNames[] = $this->settings['prefix'] . $group; - } - } + /** + * Increments the group value to simulate deletion of all keys under a group + * old values will remain in storage until they expire. + * + * @param string $group The group to clear. + * @return bool success + */ + public function clearGroup($group) + { + $success = null; + wincache_ucache_inc($this->settings['prefix'] . $group, 1, $success); + return $success; + } - $groups = wincache_ucache_get($this->_compiledGroupNames); - if (count($groups) !== count($this->settings['groups'])) { - foreach ($this->_compiledGroupNames as $group) { - if (!isset($groups[$group])) { - wincache_ucache_set($group, 1); - $groups[$group] = 1; - } - } - ksort($groups); - } + /** + * Write data for key into cache if it doesn't exist already. + * If it already exists, it fails and returns false. + * + * @param string $key Identifier for the data. + * @param mixed $value Data to be cached. + * @param int $duration How long to cache the data, in seconds. + * @return bool True if the data was successfully cached, false on failure. + */ + public function add($key, $value, $duration) + { + $cachedValue = $this->read($key); + if ($cachedValue === false) { + return $this->write($key, $value, $duration); + } + return false; + } - $result = array(); - $groups = array_values($groups); - foreach ($this->settings['groups'] as $i => $group) { - $result[] = $group . $groups[$i]; - } - return $result; - } + /** + * Read a key from the cache + * + * @param string $key Identifier for the data + * @return mixed The cached data, or false if the data doesn't exist, has expired, or if + * there was an error fetching it + */ + public function read($key) + { + $time = time(); + $cachetime = (int)wincache_ucache_get($key . '_expires'); + if ($cachetime < $time || ($time + $this->settings['duration']) < $cachetime) { + return false; + } + return wincache_ucache_get($key); + } -/** - * Increments the group value to simulate deletion of all keys under a group - * old values will remain in storage until they expire. - * - * @param string $group The group to clear. - * @return bool success - */ - public function clearGroup($group) { - $success = null; - wincache_ucache_inc($this->settings['prefix'] . $group, 1, $success); - return $success; - } + /** + * Write data for key into cache + * + * @param string $key Identifier for the data + * @param mixed $value Data to be cached + * @param int $duration How long to cache the data, in seconds + * @return bool True if the data was successfully cached, false on failure + */ + public function write($key, $value, $duration) + { + $expires = time() + $duration; -/** - * Write data for key into cache if it doesn't exist already. - * If it already exists, it fails and returns false. - * - * @param string $key Identifier for the data. - * @param mixed $value Data to be cached. - * @param int $duration How long to cache the data, in seconds. - * @return bool True if the data was successfully cached, false on failure. - */ - public function add($key, $value, $duration) { - $cachedValue = $this->read($key); - if ($cachedValue === false) { - return $this->write($key, $value, $duration); - } - return false; - } + $data = [ + $key . '_expires' => $expires, + $key => $value + ]; + $result = wincache_ucache_set($data, null, $duration); + return empty($result); + } } diff --git a/lib/Cake/Cache/Engine/XcacheEngine.php b/lib/Cake/Cache/Engine/XcacheEngine.php index 2ce3324f..ca9cd3a1 100755 --- a/lib/Cake/Cache/Engine/XcacheEngine.php +++ b/lib/Cake/Cache/Engine/XcacheEngine.php @@ -22,206 +22,218 @@ * @link http://trac.lighttpd.net/xcache/ Xcache * @package Cake.Cache.Engine */ -class XcacheEngine extends CacheEngine { +class XcacheEngine extends CacheEngine +{ -/** - * Settings - * - * - PHP_AUTH_USER = xcache.admin.user, default cake - * - PHP_AUTH_PW = xcache.admin.password, default cake - * - * @var array - */ - public $settings = array(); + /** + * Settings + * + * - PHP_AUTH_USER = xcache.admin.user, default cake + * - PHP_AUTH_PW = xcache.admin.password, default cake + * + * @var array + */ + public $settings = []; -/** - * Initialize the Cache Engine - * - * Called automatically by the cache frontend - * To reinitialize the settings call Cache::engine('EngineName', [optional] settings = array()); - * - * @param array $settings array of setting for the engine - * @return bool True if the engine has been successfully initialized, false if not - */ - public function init($settings = array()) { - if (PHP_SAPI !== 'cli') { - parent::init(array_merge(array( - 'engine' => 'Xcache', - 'prefix' => Inflector::slug(APP_DIR) . '_', - 'PHP_AUTH_USER' => 'user', - 'PHP_AUTH_PW' => 'password' - ), $settings) - ); - return function_exists('xcache_info'); - } - return false; - } + /** + * Initialize the Cache Engine + * + * Called automatically by the cache frontend + * To reinitialize the settings call Cache::engine('EngineName', [optional] settings = array()); + * + * @param array $settings array of setting for the engine + * @return bool True if the engine has been successfully initialized, false if not + */ + public function init($settings = []) + { + if (PHP_SAPI !== 'cli') { + parent::init(array_merge([ + 'engine' => 'Xcache', + 'prefix' => Inflector::slug(APP_DIR) . '_', + 'PHP_AUTH_USER' => 'user', + 'PHP_AUTH_PW' => 'password' + ], $settings) + ); + return function_exists('xcache_info'); + } + return false; + } -/** - * Write data for key into cache - * - * @param string $key Identifier for the data - * @param mixed $value Data to be cached - * @param int $duration How long to cache the data, in seconds - * @return bool True if the data was successfully cached, false on failure - */ - public function write($key, $value, $duration) { - $expires = time() + $duration; - xcache_set($key . '_expires', $expires, $duration); - return xcache_set($key, $value, $duration); - } + /** + * Increments the value of an integer cached key + * If the cache key is not an integer it will be treated as 0 + * + * @param string $key Identifier for the data + * @param int $offset How much to increment + * @return New incremented value, false otherwise + */ + public function increment($key, $offset = 1) + { + return xcache_inc($key, $offset); + } -/** - * Read a key from the cache - * - * @param string $key Identifier for the data - * @return mixed The cached data, or false if the data doesn't exist, has expired, or if there was an error fetching it - */ - public function read($key) { - if (xcache_isset($key)) { - $time = time(); - $cachetime = (int)xcache_get($key . '_expires'); - if ($cachetime < $time || ($time + $this->settings['duration']) < $cachetime) { - return false; - } - return xcache_get($key); - } - return false; - } + /** + * Decrements the value of an integer cached key. + * If the cache key is not an integer it will be treated as 0 + * + * @param string $key Identifier for the data + * @param int $offset How much to subtract + * @return New decremented value, false otherwise + */ + public function decrement($key, $offset = 1) + { + return xcache_dec($key, $offset); + } -/** - * Increments the value of an integer cached key - * If the cache key is not an integer it will be treated as 0 - * - * @param string $key Identifier for the data - * @param int $offset How much to increment - * @return New incremented value, false otherwise - */ - public function increment($key, $offset = 1) { - return xcache_inc($key, $offset); - } + /** + * Delete a key from the cache + * + * @param string $key Identifier for the data + * @return bool True if the value was successfully deleted, false if it didn't exist or couldn't be removed + */ + public function delete($key) + { + return xcache_unset($key); + } -/** - * Decrements the value of an integer cached key. - * If the cache key is not an integer it will be treated as 0 - * - * @param string $key Identifier for the data - * @param int $offset How much to subtract - * @return New decremented value, false otherwise - */ - public function decrement($key, $offset = 1) { - return xcache_dec($key, $offset); - } + /** + * Delete all keys from the cache + * + * @param bool $check If true no deletes will occur and instead CakePHP will rely + * on key TTL values. + * @return bool True if the cache was successfully cleared, false otherwise + */ + public function clear($check) + { + $this->_auth(); + $max = xcache_count(XC_TYPE_VAR); + for ($i = 0; $i < $max; $i++) { + xcache_clear_cache(XC_TYPE_VAR, $i); + } + $this->_auth(true); + return true; + } -/** - * Delete a key from the cache - * - * @param string $key Identifier for the data - * @return bool True if the value was successfully deleted, false if it didn't exist or couldn't be removed - */ - public function delete($key) { - return xcache_unset($key); - } + /** + * Populates and reverses $_SERVER authentication values + * Makes necessary changes (and reverting them back) in $_SERVER + * + * This has to be done because xcache_clear_cache() needs to pass Basic Http Auth + * (see xcache.admin configuration settings) + * + * @param bool $reverse Revert changes + * @return void + */ + protected function _auth($reverse = false) + { + static $backup = []; + $keys = ['PHP_AUTH_USER' => 'user', 'PHP_AUTH_PW' => 'password']; + foreach ($keys as $key => $setting) { + if ($reverse) { + if (isset($backup[$key])) { + $_SERVER[$key] = $backup[$key]; + unset($backup[$key]); + } else { + unset($_SERVER[$key]); + } + } else { + $value = env($key); + if (!empty($value)) { + $backup[$key] = $value; + } + if (!empty($this->settings[$setting])) { + $_SERVER[$key] = $this->settings[$setting]; + } else if (!empty($this->settings[$key])) { + $_SERVER[$key] = $this->settings[$key]; + } else { + $_SERVER[$key] = $value; + } + } + } + } -/** - * Delete all keys from the cache - * - * @param bool $check If true no deletes will occur and instead CakePHP will rely - * on key TTL values. - * @return bool True if the cache was successfully cleared, false otherwise - */ - public function clear($check) { - $this->_auth(); - $max = xcache_count(XC_TYPE_VAR); - for ($i = 0; $i < $max; $i++) { - xcache_clear_cache(XC_TYPE_VAR, $i); - } - $this->_auth(true); - return true; - } + /** + * Returns the `group value` for each of the configured groups + * If the group initial value was not found, then it initializes + * the group accordingly. + * + * @return array + */ + public function groups() + { + $result = []; + foreach ($this->settings['groups'] as $group) { + $value = xcache_get($this->settings['prefix'] . $group); + if (!$value) { + $value = 1; + xcache_set($this->settings['prefix'] . $group, $value, 0); + } + $result[] = $group . $value; + } + return $result; + } -/** - * Returns the `group value` for each of the configured groups - * If the group initial value was not found, then it initializes - * the group accordingly. - * - * @return array - */ - public function groups() { - $result = array(); - foreach ($this->settings['groups'] as $group) { - $value = xcache_get($this->settings['prefix'] . $group); - if (!$value) { - $value = 1; - xcache_set($this->settings['prefix'] . $group, $value, 0); - } - $result[] = $group . $value; - } - return $result; - } + /** + * Increments the group value to simulate deletion of all keys under a group + * old values will remain in storage until they expire. + * + * @param string $group The group to clear. + * @return bool success + */ + public function clearGroup($group) + { + return (bool)xcache_inc($this->settings['prefix'] . $group, 1); + } -/** - * Increments the group value to simulate deletion of all keys under a group - * old values will remain in storage until they expire. - * - * @param string $group The group to clear. - * @return bool success - */ - public function clearGroup($group) { - return (bool)xcache_inc($this->settings['prefix'] . $group, 1); - } + /** + * Write data for key into cache if it doesn't exist already. + * If it already exists, it fails and returns false. + * + * @param string $key Identifier for the data. + * @param mixed $value Data to be cached. + * @param int $duration How long to cache the data, in seconds. + * @return bool True if the data was successfully cached, false on failure. + */ + public function add($key, $value, $duration) + { + $cachedValue = $this->read($key); + if ($cachedValue === false) { + return $this->write($key, $value, $duration); + } + return false; + } -/** - * Populates and reverses $_SERVER authentication values - * Makes necessary changes (and reverting them back) in $_SERVER - * - * This has to be done because xcache_clear_cache() needs to pass Basic Http Auth - * (see xcache.admin configuration settings) - * - * @param bool $reverse Revert changes - * @return void - */ - protected function _auth($reverse = false) { - static $backup = array(); - $keys = array('PHP_AUTH_USER' => 'user', 'PHP_AUTH_PW' => 'password'); - foreach ($keys as $key => $setting) { - if ($reverse) { - if (isset($backup[$key])) { - $_SERVER[$key] = $backup[$key]; - unset($backup[$key]); - } else { - unset($_SERVER[$key]); - } - } else { - $value = env($key); - if (!empty($value)) { - $backup[$key] = $value; - } - if (!empty($this->settings[$setting])) { - $_SERVER[$key] = $this->settings[$setting]; - } elseif (!empty($this->settings[$key])) { - $_SERVER[$key] = $this->settings[$key]; - } else { - $_SERVER[$key] = $value; - } - } - } - } + /** + * Read a key from the cache + * + * @param string $key Identifier for the data + * @return mixed The cached data, or false if the data doesn't exist, has expired, or if there was an error fetching it + */ + public function read($key) + { + if (xcache_isset($key)) { + $time = time(); + $cachetime = (int)xcache_get($key . '_expires'); + if ($cachetime < $time || ($time + $this->settings['duration']) < $cachetime) { + return false; + } + return xcache_get($key); + } + return false; + } -/** - * Write data for key into cache if it doesn't exist already. - * If it already exists, it fails and returns false. - * - * @param string $key Identifier for the data. - * @param mixed $value Data to be cached. - * @param int $duration How long to cache the data, in seconds. - * @return bool True if the data was successfully cached, false on failure. - */ - public function add($key, $value, $duration) { - $cachedValue = $this->read($key); - if ($cachedValue === false) { - return $this->write($key, $value, $duration); - } - return false; - } + /** + * Write data for key into cache + * + * @param string $key Identifier for the data + * @param mixed $value Data to be cached + * @param int $duration How long to cache the data, in seconds + * @return bool True if the data was successfully cached, false on failure + */ + public function write($key, $value, $duration) + { + $expires = time() + $duration; + xcache_set($key . '_expires', $expires, $duration); + return xcache_set($key, $value, $duration); + } } diff --git a/lib/Cake/Config/routes.php b/lib/Cake/Config/routes.php index 5d603ed8..257c5834 100755 --- a/lib/Cake/Config/routes.php +++ b/lib/Cake/Config/routes.php @@ -43,40 +43,40 @@ $prefixes = Router::prefixes(); if ($plugins = CakePlugin::loaded()) { - App::uses('PluginShortRoute', 'Routing/Route'); - foreach ($plugins as $key => $value) { - $plugins[$key] = Inflector::underscore($value); - } - $pluginPattern = implode('|', $plugins); - $match = array('plugin' => $pluginPattern, 'defaultRoute' => true); - $shortParams = array('routeClass' => 'PluginShortRoute', 'plugin' => $pluginPattern, 'defaultRoute' => true); + App::uses('PluginShortRoute', 'Routing/Route'); + foreach ($plugins as $key => $value) { + $plugins[$key] = Inflector::underscore($value); + } + $pluginPattern = implode('|', $plugins); + $match = ['plugin' => $pluginPattern, 'defaultRoute' => true]; + $shortParams = ['routeClass' => 'PluginShortRoute', 'plugin' => $pluginPattern, 'defaultRoute' => true]; - foreach ($prefixes as $prefix) { - $params = array('prefix' => $prefix, $prefix => true); - $indexParams = $params + array('action' => 'index'); - Router::connect("/{$prefix}/:plugin", $indexParams, $shortParams); - Router::connect("/{$prefix}/:plugin/:controller", $indexParams, $match); - Router::connect("/{$prefix}/:plugin/:controller/:action/*", $params, $match); - } - Router::connect('/:plugin', array('action' => 'index'), $shortParams); - Router::connect('/:plugin/:controller', array('action' => 'index'), $match); - Router::connect('/:plugin/:controller/:action/*', array(), $match); + foreach ($prefixes as $prefix) { + $params = ['prefix' => $prefix, $prefix => true]; + $indexParams = $params + ['action' => 'index']; + Router::connect("/{$prefix}/:plugin", $indexParams, $shortParams); + Router::connect("/{$prefix}/:plugin/:controller", $indexParams, $match); + Router::connect("/{$prefix}/:plugin/:controller/:action/*", $params, $match); + } + Router::connect('/:plugin', ['action' => 'index'], $shortParams); + Router::connect('/:plugin/:controller', ['action' => 'index'], $match); + Router::connect('/:plugin/:controller/:action/*', [], $match); } foreach ($prefixes as $prefix) { - $params = array('prefix' => $prefix, $prefix => true); - $indexParams = $params + array('action' => 'index'); - Router::connect("/{$prefix}/:controller", $indexParams, array('defaultRoute' => true)); - Router::connect("/{$prefix}/:controller/:action/*", $params, array('defaultRoute' => true)); + $params = ['prefix' => $prefix, $prefix => true]; + $indexParams = $params + ['action' => 'index']; + Router::connect("/{$prefix}/:controller", $indexParams, ['defaultRoute' => true]); + Router::connect("/{$prefix}/:controller/:action/*", $params, ['defaultRoute' => true]); } -Router::connect('/:controller', array('action' => 'index'), array('defaultRoute' => true)); -Router::connect('/:controller/:action/*', array(), array('defaultRoute' => true)); +Router::connect('/:controller', ['action' => 'index'], ['defaultRoute' => true]); +Router::connect('/:controller/:action/*', [], ['defaultRoute' => true]); $namedConfig = Router::namedConfig(); if ($namedConfig['rules'] === false) { - Router::connectNamed(true); + Router::connectNamed(true); } unset($namedConfig, $params, $indexParams, $prefix, $prefixes, $shortParams, $match, - $pluginPattern, $plugins, $key, $value); + $pluginPattern, $plugins, $key, $value); diff --git a/lib/Cake/Config/unicode/casefolding/0080_00ff.php b/lib/Cake/Config/unicode/casefolding/0080_00ff.php index f79b4df6..236bd45b 100755 --- a/lib/Cake/Config/unicode/casefolding/0080_00ff.php +++ b/lib/Cake/Config/unicode/casefolding/0080_00ff.php @@ -27,7 +27,7 @@ * * The lower filed is an array of the decimal values that form the lower case version of a character. * - * The status field is: + * The status field is: * C: common case folding, common mappings shared by both simple and full mappings. * F: full case folding, mappings that cause strings to grow in length. Multiple characters are separated by spaces. * S: simple case folding, mappings to single characters where different from F. @@ -37,36 +37,36 @@ * Note that the Turkic mappings do not maintain canonical equivalence without additional processing. * See the discussions of case mapping in the Unicode Standard for more information. */ -$config['0080_00ff'][] = array('upper' => 181, 'status' => 'C', 'lower' => array(956)); -$config['0080_00ff'][] = array('upper' => 924, 'status' => 'C', 'lower' => array(181)); -$config['0080_00ff'][] = array('upper' => 192, 'status' => 'C', 'lower' => array(224)); /* LATIN CAPITAL LETTER A WITH GRAVE */ -$config['0080_00ff'][] = array('upper' => 193, 'status' => 'C', 'lower' => array(225)); /* LATIN CAPITAL LETTER A WITH ACUTE */ -$config['0080_00ff'][] = array('upper' => 194, 'status' => 'C', 'lower' => array(226)); /* LATIN CAPITAL LETTER A WITH CIRCUMFLEX */ -$config['0080_00ff'][] = array('upper' => 195, 'status' => 'C', 'lower' => array(227)); /* LATIN CAPITAL LETTER A WITH TILDE */ -$config['0080_00ff'][] = array('upper' => 196, 'status' => 'C', 'lower' => array(228)); /* LATIN CAPITAL LETTER A WITH DIAERESIS */ -$config['0080_00ff'][] = array('upper' => 197, 'status' => 'C', 'lower' => array(229)); /* LATIN CAPITAL LETTER A WITH RING ABOVE */ -$config['0080_00ff'][] = array('upper' => 198, 'status' => 'C', 'lower' => array(230)); /* LATIN CAPITAL LETTER AE */ -$config['0080_00ff'][] = array('upper' => 199, 'status' => 'C', 'lower' => array(231)); /* LATIN CAPITAL LETTER C WITH CEDILLA */ -$config['0080_00ff'][] = array('upper' => 200, 'status' => 'C', 'lower' => array(232)); /* LATIN CAPITAL LETTER E WITH GRAVE */ -$config['0080_00ff'][] = array('upper' => 201, 'status' => 'C', 'lower' => array(233)); /* LATIN CAPITAL LETTER E WITH ACUTE */ -$config['0080_00ff'][] = array('upper' => 202, 'status' => 'C', 'lower' => array(234)); /* LATIN CAPITAL LETTER E WITH CIRCUMFLEX */ -$config['0080_00ff'][] = array('upper' => 203, 'status' => 'C', 'lower' => array(235)); /* LATIN CAPITAL LETTER E WITH DIAERESIS */ -$config['0080_00ff'][] = array('upper' => 204, 'status' => 'C', 'lower' => array(236)); /* LATIN CAPITAL LETTER I WITH GRAVE */ -$config['0080_00ff'][] = array('upper' => 205, 'status' => 'C', 'lower' => array(237)); /* LATIN CAPITAL LETTER I WITH ACUTE */ -$config['0080_00ff'][] = array('upper' => 206, 'status' => 'C', 'lower' => array(238)); /* LATIN CAPITAL LETTER I WITH CIRCUMFLEX */ -$config['0080_00ff'][] = array('upper' => 207, 'status' => 'C', 'lower' => array(239)); /* LATIN CAPITAL LETTER I WITH DIAERESIS */ -$config['0080_00ff'][] = array('upper' => 208, 'status' => 'C', 'lower' => array(240)); /* LATIN CAPITAL LETTER ETH */ -$config['0080_00ff'][] = array('upper' => 209, 'status' => 'C', 'lower' => array(241)); /* LATIN CAPITAL LETTER N WITH TILDE */ -$config['0080_00ff'][] = array('upper' => 210, 'status' => 'C', 'lower' => array(242)); /* LATIN CAPITAL LETTER O WITH GRAVE */ -$config['0080_00ff'][] = array('upper' => 211, 'status' => 'C', 'lower' => array(243)); /* LATIN CAPITAL LETTER O WITH ACUTE */ -$config['0080_00ff'][] = array('upper' => 212, 'status' => 'C', 'lower' => array(244)); /* LATIN CAPITAL LETTER O WITH CIRCUMFLEX */ -$config['0080_00ff'][] = array('upper' => 213, 'status' => 'C', 'lower' => array(245)); /* LATIN CAPITAL LETTER O WITH TILDE */ -$config['0080_00ff'][] = array('upper' => 214, 'status' => 'C', 'lower' => array(246)); /* LATIN CAPITAL LETTER O WITH DIAERESIS */ -$config['0080_00ff'][] = array('upper' => 216, 'status' => 'C', 'lower' => array(248)); /* LATIN CAPITAL LETTER O WITH STROKE */ -$config['0080_00ff'][] = array('upper' => 217, 'status' => 'C', 'lower' => array(249)); /* LATIN CAPITAL LETTER U WITH GRAVE */ -$config['0080_00ff'][] = array('upper' => 218, 'status' => 'C', 'lower' => array(250)); /* LATIN CAPITAL LETTER U WITH ACUTE */ -$config['0080_00ff'][] = array('upper' => 219, 'status' => 'C', 'lower' => array(251)); /* LATIN CAPITAL LETTER U WITH CIRCUMFLEX */ -$config['0080_00ff'][] = array('upper' => 220, 'status' => 'C', 'lower' => array(252)); /* LATIN CAPITAL LETTER U WITH DIAERESIS */ -$config['0080_00ff'][] = array('upper' => 221, 'status' => 'C', 'lower' => array(253)); /* LATIN CAPITAL LETTER Y WITH ACUTE */ -$config['0080_00ff'][] = array('upper' => 222, 'status' => 'C', 'lower' => array(254)); /* LATIN CAPITAL LETTER THORN */ -$config['0080_00ff'][] = array('upper' => 223, 'status' => 'F', 'lower' => array(115, 115)); /* LATIN SMALL LETTER SHARP S */ +$config['0080_00ff'][] = ['upper' => 181, 'status' => 'C', 'lower' => [956]]; +$config['0080_00ff'][] = ['upper' => 924, 'status' => 'C', 'lower' => [181]]; +$config['0080_00ff'][] = ['upper' => 192, 'status' => 'C', 'lower' => [224]]; /* LATIN CAPITAL LETTER A WITH GRAVE */ +$config['0080_00ff'][] = ['upper' => 193, 'status' => 'C', 'lower' => [225]]; /* LATIN CAPITAL LETTER A WITH ACUTE */ +$config['0080_00ff'][] = ['upper' => 194, 'status' => 'C', 'lower' => [226]]; /* LATIN CAPITAL LETTER A WITH CIRCUMFLEX */ +$config['0080_00ff'][] = ['upper' => 195, 'status' => 'C', 'lower' => [227]]; /* LATIN CAPITAL LETTER A WITH TILDE */ +$config['0080_00ff'][] = ['upper' => 196, 'status' => 'C', 'lower' => [228]]; /* LATIN CAPITAL LETTER A WITH DIAERESIS */ +$config['0080_00ff'][] = ['upper' => 197, 'status' => 'C', 'lower' => [229]]; /* LATIN CAPITAL LETTER A WITH RING ABOVE */ +$config['0080_00ff'][] = ['upper' => 198, 'status' => 'C', 'lower' => [230]]; /* LATIN CAPITAL LETTER AE */ +$config['0080_00ff'][] = ['upper' => 199, 'status' => 'C', 'lower' => [231]]; /* LATIN CAPITAL LETTER C WITH CEDILLA */ +$config['0080_00ff'][] = ['upper' => 200, 'status' => 'C', 'lower' => [232]]; /* LATIN CAPITAL LETTER E WITH GRAVE */ +$config['0080_00ff'][] = ['upper' => 201, 'status' => 'C', 'lower' => [233]]; /* LATIN CAPITAL LETTER E WITH ACUTE */ +$config['0080_00ff'][] = ['upper' => 202, 'status' => 'C', 'lower' => [234]]; /* LATIN CAPITAL LETTER E WITH CIRCUMFLEX */ +$config['0080_00ff'][] = ['upper' => 203, 'status' => 'C', 'lower' => [235]]; /* LATIN CAPITAL LETTER E WITH DIAERESIS */ +$config['0080_00ff'][] = ['upper' => 204, 'status' => 'C', 'lower' => [236]]; /* LATIN CAPITAL LETTER I WITH GRAVE */ +$config['0080_00ff'][] = ['upper' => 205, 'status' => 'C', 'lower' => [237]]; /* LATIN CAPITAL LETTER I WITH ACUTE */ +$config['0080_00ff'][] = ['upper' => 206, 'status' => 'C', 'lower' => [238]]; /* LATIN CAPITAL LETTER I WITH CIRCUMFLEX */ +$config['0080_00ff'][] = ['upper' => 207, 'status' => 'C', 'lower' => [239]]; /* LATIN CAPITAL LETTER I WITH DIAERESIS */ +$config['0080_00ff'][] = ['upper' => 208, 'status' => 'C', 'lower' => [240]]; /* LATIN CAPITAL LETTER ETH */ +$config['0080_00ff'][] = ['upper' => 209, 'status' => 'C', 'lower' => [241]]; /* LATIN CAPITAL LETTER N WITH TILDE */ +$config['0080_00ff'][] = ['upper' => 210, 'status' => 'C', 'lower' => [242]]; /* LATIN CAPITAL LETTER O WITH GRAVE */ +$config['0080_00ff'][] = ['upper' => 211, 'status' => 'C', 'lower' => [243]]; /* LATIN CAPITAL LETTER O WITH ACUTE */ +$config['0080_00ff'][] = ['upper' => 212, 'status' => 'C', 'lower' => [244]]; /* LATIN CAPITAL LETTER O WITH CIRCUMFLEX */ +$config['0080_00ff'][] = ['upper' => 213, 'status' => 'C', 'lower' => [245]]; /* LATIN CAPITAL LETTER O WITH TILDE */ +$config['0080_00ff'][] = ['upper' => 214, 'status' => 'C', 'lower' => [246]]; /* LATIN CAPITAL LETTER O WITH DIAERESIS */ +$config['0080_00ff'][] = ['upper' => 216, 'status' => 'C', 'lower' => [248]]; /* LATIN CAPITAL LETTER O WITH STROKE */ +$config['0080_00ff'][] = ['upper' => 217, 'status' => 'C', 'lower' => [249]]; /* LATIN CAPITAL LETTER U WITH GRAVE */ +$config['0080_00ff'][] = ['upper' => 218, 'status' => 'C', 'lower' => [250]]; /* LATIN CAPITAL LETTER U WITH ACUTE */ +$config['0080_00ff'][] = ['upper' => 219, 'status' => 'C', 'lower' => [251]]; /* LATIN CAPITAL LETTER U WITH CIRCUMFLEX */ +$config['0080_00ff'][] = ['upper' => 220, 'status' => 'C', 'lower' => [252]]; /* LATIN CAPITAL LETTER U WITH DIAERESIS */ +$config['0080_00ff'][] = ['upper' => 221, 'status' => 'C', 'lower' => [253]]; /* LATIN CAPITAL LETTER Y WITH ACUTE */ +$config['0080_00ff'][] = ['upper' => 222, 'status' => 'C', 'lower' => [254]]; /* LATIN CAPITAL LETTER THORN */ +$config['0080_00ff'][] = ['upper' => 223, 'status' => 'F', 'lower' => [115, 115]]; /* LATIN SMALL LETTER SHARP S */ diff --git a/lib/Cake/Config/unicode/casefolding/0100_017f.php b/lib/Cake/Config/unicode/casefolding/0100_017f.php index 236311c1..a023d84b 100755 --- a/lib/Cake/Config/unicode/casefolding/0100_017f.php +++ b/lib/Cake/Config/unicode/casefolding/0100_017f.php @@ -27,7 +27,7 @@ * * The lower filed is an array of the decimal values that form the lower case version of a character. * - * The status field is: + * The status field is: * C: common case folding, common mappings shared by both simple and full mappings. * F: full case folding, mappings that cause strings to grow in length. Multiple characters are separated by spaces. * S: simple case folding, mappings to single characters where different from F. @@ -37,69 +37,69 @@ * Note that the Turkic mappings do not maintain canonical equivalence without additional processing. * See the discussions of case mapping in the Unicode Standard for more information. */ -$config['0100_017f'][] = array('upper' => 256, 'status' => 'C', 'lower' => array(257)); /* LATIN CAPITAL LETTER A WITH MACRON */ -$config['0100_017f'][] = array('upper' => 258, 'status' => 'C', 'lower' => array(259)); /* LATIN CAPITAL LETTER A WITH BREVE */ -$config['0100_017f'][] = array('upper' => 260, 'status' => 'C', 'lower' => array(261)); /* LATIN CAPITAL LETTER A WITH OGONEK */ -$config['0100_017f'][] = array('upper' => 262, 'status' => 'C', 'lower' => array(263)); /* LATIN CAPITAL LETTER C WITH ACUTE */ -$config['0100_017f'][] = array('upper' => 264, 'status' => 'C', 'lower' => array(265)); /* LATIN CAPITAL LETTER C WITH CIRCUMFLEX */ -$config['0100_017f'][] = array('upper' => 266, 'status' => 'C', 'lower' => array(267)); /* LATIN CAPITAL LETTER C WITH DOT ABOVE */ -$config['0100_017f'][] = array('upper' => 268, 'status' => 'C', 'lower' => array(269)); /* LATIN CAPITAL LETTER C WITH CARON */ -$config['0100_017f'][] = array('upper' => 270, 'status' => 'C', 'lower' => array(271)); /* LATIN CAPITAL LETTER D WITH CARON */ -$config['0100_017f'][] = array('upper' => 272, 'status' => 'C', 'lower' => array(273)); /* LATIN CAPITAL LETTER D WITH STROKE */ -$config['0100_017f'][] = array('upper' => 274, 'status' => 'C', 'lower' => array(275)); /* LATIN CAPITAL LETTER E WITH MACRON */ -$config['0100_017f'][] = array('upper' => 276, 'status' => 'C', 'lower' => array(277)); /* LATIN CAPITAL LETTER E WITH BREVE */ -$config['0100_017f'][] = array('upper' => 278, 'status' => 'C', 'lower' => array(279)); /* LATIN CAPITAL LETTER E WITH DOT ABOVE */ -$config['0100_017f'][] = array('upper' => 280, 'status' => 'C', 'lower' => array(281)); /* LATIN CAPITAL LETTER E WITH OGONEK */ -$config['0100_017f'][] = array('upper' => 282, 'status' => 'C', 'lower' => array(283)); /* LATIN CAPITAL LETTER E WITH CARON */ -$config['0100_017f'][] = array('upper' => 284, 'status' => 'C', 'lower' => array(285)); /* LATIN CAPITAL LETTER G WITH CIRCUMFLEX */ -$config['0100_017f'][] = array('upper' => 286, 'status' => 'C', 'lower' => array(287)); /* LATIN CAPITAL LETTER G WITH BREVE */ -$config['0100_017f'][] = array('upper' => 288, 'status' => 'C', 'lower' => array(289)); /* LATIN CAPITAL LETTER G WITH DOT ABOVE */ -$config['0100_017f'][] = array('upper' => 290, 'status' => 'C', 'lower' => array(291)); /* LATIN CAPITAL LETTER G WITH CEDILLA */ -$config['0100_017f'][] = array('upper' => 292, 'status' => 'C', 'lower' => array(293)); /* LATIN CAPITAL LETTER H WITH CIRCUMFLEX */ -$config['0100_017f'][] = array('upper' => 294, 'status' => 'C', 'lower' => array(295)); /* LATIN CAPITAL LETTER H WITH STROKE */ -$config['0100_017f'][] = array('upper' => 296, 'status' => 'C', 'lower' => array(297)); /* LATIN CAPITAL LETTER I WITH TILDE */ -$config['0100_017f'][] = array('upper' => 298, 'status' => 'C', 'lower' => array(299)); /* LATIN CAPITAL LETTER I WITH MACRON */ -$config['0100_017f'][] = array('upper' => 300, 'status' => 'C', 'lower' => array(301)); /* LATIN CAPITAL LETTER I WITH BREVE */ -$config['0100_017f'][] = array('upper' => 302, 'status' => 'C', 'lower' => array(303)); /* LATIN CAPITAL LETTER I WITH OGONEK */ -$config['0100_017f'][] = array('upper' => 304, 'status' => 'F', 'lower' => array(105, 775)); /* LATIN CAPITAL LETTER I WITH DOT ABOVE */ -$config['0100_017f'][] = array('upper' => 304, 'status' => 'T', 'lower' => array(105)); /* LATIN CAPITAL LETTER I WITH DOT ABOVE */ -$config['0100_017f'][] = array('upper' => 306, 'status' => 'C', 'lower' => array(307)); /* LATIN CAPITAL LIGATURE IJ */ -$config['0100_017f'][] = array('upper' => 308, 'status' => 'C', 'lower' => array(309)); /* LATIN CAPITAL LETTER J WITH CIRCUMFLEX */ -$config['0100_017f'][] = array('upper' => 310, 'status' => 'C', 'lower' => array(311)); /* LATIN CAPITAL LETTER K WITH CEDILLA */ -$config['0100_017f'][] = array('upper' => 313, 'status' => 'C', 'lower' => array(314)); /* LATIN CAPITAL LETTER L WITH ACUTE */ -$config['0100_017f'][] = array('upper' => 315, 'status' => 'C', 'lower' => array(316)); /* LATIN CAPITAL LETTER L WITH CEDILLA */ -$config['0100_017f'][] = array('upper' => 317, 'status' => 'C', 'lower' => array(318)); /* LATIN CAPITAL LETTER L WITH CARON */ -$config['0100_017f'][] = array('upper' => 319, 'status' => 'C', 'lower' => array(320)); /* LATIN CAPITAL LETTER L WITH MIDDLE DOT */ -$config['0100_017f'][] = array('upper' => 321, 'status' => 'C', 'lower' => array(322)); /* LATIN CAPITAL LETTER L WITH STROKE */ -$config['0100_017f'][] = array('upper' => 323, 'status' => 'C', 'lower' => array(324)); /* LATIN CAPITAL LETTER N WITH ACUTE */ -$config['0100_017f'][] = array('upper' => 325, 'status' => 'C', 'lower' => array(326)); /* LATIN CAPITAL LETTER N WITH CEDILLA */ -$config['0100_017f'][] = array('upper' => 327, 'status' => 'C', 'lower' => array(328)); /* LATIN CAPITAL LETTER N WITH CARON */ -$config['0100_017f'][] = array('upper' => 329, 'status' => 'F', 'lower' => array(700, 110)); /* LATIN SMALL LETTER N PRECEDED BY APOSTROPHE */ -$config['0100_017f'][] = array('upper' => 330, 'status' => 'C', 'lower' => array(331)); /* LATIN CAPITAL LETTER ENG */ -$config['0100_017f'][] = array('upper' => 332, 'status' => 'C', 'lower' => array(333)); /* LATIN CAPITAL LETTER O WITH MACRON */ -$config['0100_017f'][] = array('upper' => 334, 'status' => 'C', 'lower' => array(335)); /* LATIN CAPITAL LETTER O WITH BREVE */ -$config['0100_017f'][] = array('upper' => 336, 'status' => 'C', 'lower' => array(337)); /* LATIN CAPITAL LETTER O WITH DOUBLE ACUTE */ -$config['0100_017f'][] = array('upper' => 338, 'status' => 'C', 'lower' => array(339)); /* LATIN CAPITAL LIGATURE OE */ -$config['0100_017f'][] = array('upper' => 340, 'status' => 'C', 'lower' => array(341)); /* LATIN CAPITAL LETTER R WITH ACUTE */ -$config['0100_017f'][] = array('upper' => 342, 'status' => 'C', 'lower' => array(343)); /* LATIN CAPITAL LETTER R WITH CEDILLA */ -$config['0100_017f'][] = array('upper' => 344, 'status' => 'C', 'lower' => array(345)); /* LATIN CAPITAL LETTER R WITH CARON */ -$config['0100_017f'][] = array('upper' => 346, 'status' => 'C', 'lower' => array(347)); /* LATIN CAPITAL LETTER S WITH ACUTE */ -$config['0100_017f'][] = array('upper' => 348, 'status' => 'C', 'lower' => array(349)); /* LATIN CAPITAL LETTER S WITH CIRCUMFLEX */ -$config['0100_017f'][] = array('upper' => 350, 'status' => 'C', 'lower' => array(351)); /* LATIN CAPITAL LETTER S WITH CEDILLA */ -$config['0100_017f'][] = array('upper' => 352, 'status' => 'C', 'lower' => array(353)); /* LATIN CAPITAL LETTER S WITH CARON */ -$config['0100_017f'][] = array('upper' => 354, 'status' => 'C', 'lower' => array(355)); /* LATIN CAPITAL LETTER T WITH CEDILLA */ -$config['0100_017f'][] = array('upper' => 356, 'status' => 'C', 'lower' => array(357)); /* LATIN CAPITAL LETTER T WITH CARON */ -$config['0100_017f'][] = array('upper' => 358, 'status' => 'C', 'lower' => array(359)); /* LATIN CAPITAL LETTER T WITH STROKE */ -$config['0100_017f'][] = array('upper' => 360, 'status' => 'C', 'lower' => array(361)); /* LATIN CAPITAL LETTER U WITH TILDE */ -$config['0100_017f'][] = array('upper' => 362, 'status' => 'C', 'lower' => array(363)); /* LATIN CAPITAL LETTER U WITH MACRON */ -$config['0100_017f'][] = array('upper' => 364, 'status' => 'C', 'lower' => array(365)); /* LATIN CAPITAL LETTER U WITH BREVE */ -$config['0100_017f'][] = array('upper' => 366, 'status' => 'C', 'lower' => array(367)); /* LATIN CAPITAL LETTER U WITH RING ABOVE */ -$config['0100_017f'][] = array('upper' => 368, 'status' => 'C', 'lower' => array(369)); /* LATIN CAPITAL LETTER U WITH DOUBLE ACUTE */ -$config['0100_017f'][] = array('upper' => 370, 'status' => 'C', 'lower' => array(371)); /* LATIN CAPITAL LETTER U WITH OGONEK */ -$config['0100_017f'][] = array('upper' => 372, 'status' => 'C', 'lower' => array(373)); /* LATIN CAPITAL LETTER W WITH CIRCUMFLEX */ -$config['0100_017f'][] = array('upper' => 374, 'status' => 'C', 'lower' => array(375)); /* LATIN CAPITAL LETTER Y WITH CIRCUMFLEX */ -$config['0100_017f'][] = array('upper' => 376, 'status' => 'C', 'lower' => array(255)); /* LATIN CAPITAL LETTER Y WITH DIAERESIS */ -$config['0100_017f'][] = array('upper' => 377, 'status' => 'C', 'lower' => array(378)); /* LATIN CAPITAL LETTER Z WITH ACUTE */ -$config['0100_017f'][] = array('upper' => 379, 'status' => 'C', 'lower' => array(380)); /* LATIN CAPITAL LETTER Z WITH DOT ABOVE */ -$config['0100_017f'][] = array('upper' => 381, 'status' => 'C', 'lower' => array(382)); /* LATIN CAPITAL LETTER Z WITH CARON */ -$config['0100_017f'][] = array('upper' => 383, 'status' => 'C', 'lower' => array(115)); /* LATIN SMALL LETTER LONG S */ +$config['0100_017f'][] = ['upper' => 256, 'status' => 'C', 'lower' => [257]]; /* LATIN CAPITAL LETTER A WITH MACRON */ +$config['0100_017f'][] = ['upper' => 258, 'status' => 'C', 'lower' => [259]]; /* LATIN CAPITAL LETTER A WITH BREVE */ +$config['0100_017f'][] = ['upper' => 260, 'status' => 'C', 'lower' => [261]]; /* LATIN CAPITAL LETTER A WITH OGONEK */ +$config['0100_017f'][] = ['upper' => 262, 'status' => 'C', 'lower' => [263]]; /* LATIN CAPITAL LETTER C WITH ACUTE */ +$config['0100_017f'][] = ['upper' => 264, 'status' => 'C', 'lower' => [265]]; /* LATIN CAPITAL LETTER C WITH CIRCUMFLEX */ +$config['0100_017f'][] = ['upper' => 266, 'status' => 'C', 'lower' => [267]]; /* LATIN CAPITAL LETTER C WITH DOT ABOVE */ +$config['0100_017f'][] = ['upper' => 268, 'status' => 'C', 'lower' => [269]]; /* LATIN CAPITAL LETTER C WITH CARON */ +$config['0100_017f'][] = ['upper' => 270, 'status' => 'C', 'lower' => [271]]; /* LATIN CAPITAL LETTER D WITH CARON */ +$config['0100_017f'][] = ['upper' => 272, 'status' => 'C', 'lower' => [273]]; /* LATIN CAPITAL LETTER D WITH STROKE */ +$config['0100_017f'][] = ['upper' => 274, 'status' => 'C', 'lower' => [275]]; /* LATIN CAPITAL LETTER E WITH MACRON */ +$config['0100_017f'][] = ['upper' => 276, 'status' => 'C', 'lower' => [277]]; /* LATIN CAPITAL LETTER E WITH BREVE */ +$config['0100_017f'][] = ['upper' => 278, 'status' => 'C', 'lower' => [279]]; /* LATIN CAPITAL LETTER E WITH DOT ABOVE */ +$config['0100_017f'][] = ['upper' => 280, 'status' => 'C', 'lower' => [281]]; /* LATIN CAPITAL LETTER E WITH OGONEK */ +$config['0100_017f'][] = ['upper' => 282, 'status' => 'C', 'lower' => [283]]; /* LATIN CAPITAL LETTER E WITH CARON */ +$config['0100_017f'][] = ['upper' => 284, 'status' => 'C', 'lower' => [285]]; /* LATIN CAPITAL LETTER G WITH CIRCUMFLEX */ +$config['0100_017f'][] = ['upper' => 286, 'status' => 'C', 'lower' => [287]]; /* LATIN CAPITAL LETTER G WITH BREVE */ +$config['0100_017f'][] = ['upper' => 288, 'status' => 'C', 'lower' => [289]]; /* LATIN CAPITAL LETTER G WITH DOT ABOVE */ +$config['0100_017f'][] = ['upper' => 290, 'status' => 'C', 'lower' => [291]]; /* LATIN CAPITAL LETTER G WITH CEDILLA */ +$config['0100_017f'][] = ['upper' => 292, 'status' => 'C', 'lower' => [293]]; /* LATIN CAPITAL LETTER H WITH CIRCUMFLEX */ +$config['0100_017f'][] = ['upper' => 294, 'status' => 'C', 'lower' => [295]]; /* LATIN CAPITAL LETTER H WITH STROKE */ +$config['0100_017f'][] = ['upper' => 296, 'status' => 'C', 'lower' => [297]]; /* LATIN CAPITAL LETTER I WITH TILDE */ +$config['0100_017f'][] = ['upper' => 298, 'status' => 'C', 'lower' => [299]]; /* LATIN CAPITAL LETTER I WITH MACRON */ +$config['0100_017f'][] = ['upper' => 300, 'status' => 'C', 'lower' => [301]]; /* LATIN CAPITAL LETTER I WITH BREVE */ +$config['0100_017f'][] = ['upper' => 302, 'status' => 'C', 'lower' => [303]]; /* LATIN CAPITAL LETTER I WITH OGONEK */ +$config['0100_017f'][] = ['upper' => 304, 'status' => 'F', 'lower' => [105, 775]]; /* LATIN CAPITAL LETTER I WITH DOT ABOVE */ +$config['0100_017f'][] = ['upper' => 304, 'status' => 'T', 'lower' => [105]]; /* LATIN CAPITAL LETTER I WITH DOT ABOVE */ +$config['0100_017f'][] = ['upper' => 306, 'status' => 'C', 'lower' => [307]]; /* LATIN CAPITAL LIGATURE IJ */ +$config['0100_017f'][] = ['upper' => 308, 'status' => 'C', 'lower' => [309]]; /* LATIN CAPITAL LETTER J WITH CIRCUMFLEX */ +$config['0100_017f'][] = ['upper' => 310, 'status' => 'C', 'lower' => [311]]; /* LATIN CAPITAL LETTER K WITH CEDILLA */ +$config['0100_017f'][] = ['upper' => 313, 'status' => 'C', 'lower' => [314]]; /* LATIN CAPITAL LETTER L WITH ACUTE */ +$config['0100_017f'][] = ['upper' => 315, 'status' => 'C', 'lower' => [316]]; /* LATIN CAPITAL LETTER L WITH CEDILLA */ +$config['0100_017f'][] = ['upper' => 317, 'status' => 'C', 'lower' => [318]]; /* LATIN CAPITAL LETTER L WITH CARON */ +$config['0100_017f'][] = ['upper' => 319, 'status' => 'C', 'lower' => [320]]; /* LATIN CAPITAL LETTER L WITH MIDDLE DOT */ +$config['0100_017f'][] = ['upper' => 321, 'status' => 'C', 'lower' => [322]]; /* LATIN CAPITAL LETTER L WITH STROKE */ +$config['0100_017f'][] = ['upper' => 323, 'status' => 'C', 'lower' => [324]]; /* LATIN CAPITAL LETTER N WITH ACUTE */ +$config['0100_017f'][] = ['upper' => 325, 'status' => 'C', 'lower' => [326]]; /* LATIN CAPITAL LETTER N WITH CEDILLA */ +$config['0100_017f'][] = ['upper' => 327, 'status' => 'C', 'lower' => [328]]; /* LATIN CAPITAL LETTER N WITH CARON */ +$config['0100_017f'][] = ['upper' => 329, 'status' => 'F', 'lower' => [700, 110]]; /* LATIN SMALL LETTER N PRECEDED BY APOSTROPHE */ +$config['0100_017f'][] = ['upper' => 330, 'status' => 'C', 'lower' => [331]]; /* LATIN CAPITAL LETTER ENG */ +$config['0100_017f'][] = ['upper' => 332, 'status' => 'C', 'lower' => [333]]; /* LATIN CAPITAL LETTER O WITH MACRON */ +$config['0100_017f'][] = ['upper' => 334, 'status' => 'C', 'lower' => [335]]; /* LATIN CAPITAL LETTER O WITH BREVE */ +$config['0100_017f'][] = ['upper' => 336, 'status' => 'C', 'lower' => [337]]; /* LATIN CAPITAL LETTER O WITH DOUBLE ACUTE */ +$config['0100_017f'][] = ['upper' => 338, 'status' => 'C', 'lower' => [339]]; /* LATIN CAPITAL LIGATURE OE */ +$config['0100_017f'][] = ['upper' => 340, 'status' => 'C', 'lower' => [341]]; /* LATIN CAPITAL LETTER R WITH ACUTE */ +$config['0100_017f'][] = ['upper' => 342, 'status' => 'C', 'lower' => [343]]; /* LATIN CAPITAL LETTER R WITH CEDILLA */ +$config['0100_017f'][] = ['upper' => 344, 'status' => 'C', 'lower' => [345]]; /* LATIN CAPITAL LETTER R WITH CARON */ +$config['0100_017f'][] = ['upper' => 346, 'status' => 'C', 'lower' => [347]]; /* LATIN CAPITAL LETTER S WITH ACUTE */ +$config['0100_017f'][] = ['upper' => 348, 'status' => 'C', 'lower' => [349]]; /* LATIN CAPITAL LETTER S WITH CIRCUMFLEX */ +$config['0100_017f'][] = ['upper' => 350, 'status' => 'C', 'lower' => [351]]; /* LATIN CAPITAL LETTER S WITH CEDILLA */ +$config['0100_017f'][] = ['upper' => 352, 'status' => 'C', 'lower' => [353]]; /* LATIN CAPITAL LETTER S WITH CARON */ +$config['0100_017f'][] = ['upper' => 354, 'status' => 'C', 'lower' => [355]]; /* LATIN CAPITAL LETTER T WITH CEDILLA */ +$config['0100_017f'][] = ['upper' => 356, 'status' => 'C', 'lower' => [357]]; /* LATIN CAPITAL LETTER T WITH CARON */ +$config['0100_017f'][] = ['upper' => 358, 'status' => 'C', 'lower' => [359]]; /* LATIN CAPITAL LETTER T WITH STROKE */ +$config['0100_017f'][] = ['upper' => 360, 'status' => 'C', 'lower' => [361]]; /* LATIN CAPITAL LETTER U WITH TILDE */ +$config['0100_017f'][] = ['upper' => 362, 'status' => 'C', 'lower' => [363]]; /* LATIN CAPITAL LETTER U WITH MACRON */ +$config['0100_017f'][] = ['upper' => 364, 'status' => 'C', 'lower' => [365]]; /* LATIN CAPITAL LETTER U WITH BREVE */ +$config['0100_017f'][] = ['upper' => 366, 'status' => 'C', 'lower' => [367]]; /* LATIN CAPITAL LETTER U WITH RING ABOVE */ +$config['0100_017f'][] = ['upper' => 368, 'status' => 'C', 'lower' => [369]]; /* LATIN CAPITAL LETTER U WITH DOUBLE ACUTE */ +$config['0100_017f'][] = ['upper' => 370, 'status' => 'C', 'lower' => [371]]; /* LATIN CAPITAL LETTER U WITH OGONEK */ +$config['0100_017f'][] = ['upper' => 372, 'status' => 'C', 'lower' => [373]]; /* LATIN CAPITAL LETTER W WITH CIRCUMFLEX */ +$config['0100_017f'][] = ['upper' => 374, 'status' => 'C', 'lower' => [375]]; /* LATIN CAPITAL LETTER Y WITH CIRCUMFLEX */ +$config['0100_017f'][] = ['upper' => 376, 'status' => 'C', 'lower' => [255]]; /* LATIN CAPITAL LETTER Y WITH DIAERESIS */ +$config['0100_017f'][] = ['upper' => 377, 'status' => 'C', 'lower' => [378]]; /* LATIN CAPITAL LETTER Z WITH ACUTE */ +$config['0100_017f'][] = ['upper' => 379, 'status' => 'C', 'lower' => [380]]; /* LATIN CAPITAL LETTER Z WITH DOT ABOVE */ +$config['0100_017f'][] = ['upper' => 381, 'status' => 'C', 'lower' => [382]]; /* LATIN CAPITAL LETTER Z WITH CARON */ +$config['0100_017f'][] = ['upper' => 383, 'status' => 'C', 'lower' => [115]]; /* LATIN SMALL LETTER LONG S */ diff --git a/lib/Cake/Config/unicode/casefolding/0180_024F.php b/lib/Cake/Config/unicode/casefolding/0180_024F.php index f803be4e..f8146446 100755 --- a/lib/Cake/Config/unicode/casefolding/0180_024F.php +++ b/lib/Cake/Config/unicode/casefolding/0180_024F.php @@ -27,7 +27,7 @@ * * The lower filed is an array of the decimal values that form the lower case version of a character. * - * The status field is: + * The status field is: * C: common case folding, common mappings shared by both simple and full mappings. * F: full case folding, mappings that cause strings to grow in length. Multiple characters are separated by spaces. * S: simple case folding, mappings to single characters where different from F. @@ -37,111 +37,111 @@ * Note that the Turkic mappings do not maintain canonical equivalence without additional processing. * See the discussions of case mapping in the Unicode Standard for more information. */ -$config['0180_024F'][] = array('upper' => 385, 'status' => 'C', 'lower' => array(595)); /* LATIN CAPITAL LETTER B WITH HOOK */ -$config['0180_024F'][] = array('upper' => 386, 'status' => 'C', 'lower' => array(387)); /* LATIN CAPITAL LETTER B WITH TOPBAR */ -$config['0180_024F'][] = array('upper' => 388, 'status' => 'C', 'lower' => array(389)); /* LATIN CAPITAL LETTER TONE SIX */ -$config['0180_024F'][] = array('upper' => 390, 'status' => 'C', 'lower' => array(596)); /* LATIN CAPITAL LETTER OPEN O */ -$config['0180_024F'][] = array('upper' => 391, 'status' => 'C', 'lower' => array(392)); /* LATIN CAPITAL LETTER C WITH HOOK */ -$config['0180_024F'][] = array('upper' => 393, 'status' => 'C', 'lower' => array(598)); /* LATIN CAPITAL LETTER AFRICAN D */ -$config['0180_024F'][] = array('upper' => 394, 'status' => 'C', 'lower' => array(599)); /* LATIN CAPITAL LETTER D WITH HOOK */ -$config['0180_024F'][] = array('upper' => 395, 'status' => 'C', 'lower' => array(396)); /* LATIN CAPITAL LETTER D WITH TOPBAR */ -$config['0180_024F'][] = array('upper' => 398, 'status' => 'C', 'lower' => array(477)); /* LATIN CAPITAL LETTER REVERSED E */ -$config['0180_024F'][] = array('upper' => 399, 'status' => 'C', 'lower' => array(601)); /* LATIN CAPITAL LETTER SCHWA */ -$config['0180_024F'][] = array('upper' => 400, 'status' => 'C', 'lower' => array(603)); /* LATIN CAPITAL LETTER OPEN E */ -$config['0180_024F'][] = array('upper' => 401, 'status' => 'C', 'lower' => array(402)); /* LATIN CAPITAL LETTER F WITH HOOK */ -$config['0180_024F'][] = array('upper' => 403, 'status' => 'C', 'lower' => array(608)); /* LATIN CAPITAL LETTER G WITH HOOK */ -$config['0180_024F'][] = array('upper' => 404, 'status' => 'C', 'lower' => array(611)); /* LATIN CAPITAL LETTER GAMMA */ -$config['0180_024F'][] = array('upper' => 406, 'status' => 'C', 'lower' => array(617)); /* LATIN CAPITAL LETTER IOTA */ -$config['0180_024F'][] = array('upper' => 407, 'status' => 'C', 'lower' => array(616)); /* LATIN CAPITAL LETTER I WITH STROKE */ -$config['0180_024F'][] = array('upper' => 408, 'status' => 'C', 'lower' => array(409)); /* LATIN CAPITAL LETTER K WITH HOOK */ -$config['0180_024F'][] = array('upper' => 412, 'status' => 'C', 'lower' => array(623)); /* LATIN CAPITAL LETTER TURNED M */ -$config['0180_024F'][] = array('upper' => 413, 'status' => 'C', 'lower' => array(626)); /* LATIN CAPITAL LETTER N WITH LEFT HOOK */ -$config['0180_024F'][] = array('upper' => 415, 'status' => 'C', 'lower' => array(629)); /* LATIN CAPITAL LETTER O WITH MIDDLE TILDE */ -$config['0180_024F'][] = array('upper' => 416, 'status' => 'C', 'lower' => array(417)); /* LATIN CAPITAL LETTER O WITH HORN */ -$config['0180_024F'][] = array('upper' => 418, 'status' => 'C', 'lower' => array(419)); /* LATIN CAPITAL LETTER OI */ -$config['0180_024F'][] = array('upper' => 420, 'status' => 'C', 'lower' => array(421)); /* LATIN CAPITAL LETTER P WITH HOOK */ -$config['0180_024F'][] = array('upper' => 422, 'status' => 'C', 'lower' => array(640)); /* LATIN LETTER YR */ -$config['0180_024F'][] = array('upper' => 423, 'status' => 'C', 'lower' => array(424)); /* LATIN CAPITAL LETTER TONE TWO */ -$config['0180_024F'][] = array('upper' => 425, 'status' => 'C', 'lower' => array(643)); /* LATIN CAPITAL LETTER ESH */ -$config['0180_024F'][] = array('upper' => 428, 'status' => 'C', 'lower' => array(429)); /* LATIN CAPITAL LETTER T WITH HOOK */ -$config['0180_024F'][] = array('upper' => 430, 'status' => 'C', 'lower' => array(648)); /* LATIN CAPITAL LETTER T WITH RETROFLEX HOOK */ -$config['0180_024F'][] = array('upper' => 431, 'status' => 'C', 'lower' => array(432)); /* LATIN CAPITAL LETTER U WITH HORN */ -$config['0180_024F'][] = array('upper' => 433, 'status' => 'C', 'lower' => array(650)); /* LATIN CAPITAL LETTER UPSILON */ -$config['0180_024F'][] = array('upper' => 434, 'status' => 'C', 'lower' => array(651)); /* LATIN CAPITAL LETTER V WITH HOOK */ -$config['0180_024F'][] = array('upper' => 435, 'status' => 'C', 'lower' => array(436)); /* LATIN CAPITAL LETTER Y WITH HOOK */ -$config['0180_024F'][] = array('upper' => 437, 'status' => 'C', 'lower' => array(438)); /* LATIN CAPITAL LETTER Z WITH STROKE */ -$config['0180_024F'][] = array('upper' => 439, 'status' => 'C', 'lower' => array(658)); /* LATIN CAPITAL LETTER EZH */ -$config['0180_024F'][] = array('upper' => 440, 'status' => 'C', 'lower' => array(441)); /* LATIN CAPITAL LETTER EZH REVERSED */ -$config['0180_024F'][] = array('upper' => 444, 'status' => 'C', 'lower' => array(445)); /* LATIN CAPITAL LETTER TONE FIVE */ -$config['0180_024F'][] = array('upper' => 452, 'status' => 'C', 'lower' => array(454)); /* LATIN CAPITAL LETTER DZ WITH CARON */ -$config['0180_024F'][] = array('upper' => 453, 'status' => 'C', 'lower' => array(454)); /* LATIN CAPITAL LETTER D WITH SMALL LETTER Z WITH CARON */ -$config['0180_024F'][] = array('upper' => 455, 'status' => 'C', 'lower' => array(457)); /* LATIN CAPITAL LETTER LJ */ -$config['0180_024F'][] = array('upper' => 456, 'status' => 'C', 'lower' => array(457)); /* LATIN CAPITAL LETTER L WITH SMALL LETTER J */ -$config['0180_024F'][] = array('upper' => 458, 'status' => 'C', 'lower' => array(460)); /* LATIN CAPITAL LETTER NJ */ -$config['0180_024F'][] = array('upper' => 459, 'status' => 'C', 'lower' => array(460)); /* LATIN CAPITAL LETTER N WITH SMALL LETTER J */ -$config['0180_024F'][] = array('upper' => 461, 'status' => 'C', 'lower' => array(462)); /* LATIN CAPITAL LETTER A WITH CARON */ -$config['0180_024F'][] = array('upper' => 463, 'status' => 'C', 'lower' => array(464)); /* LATIN CAPITAL LETTER I WITH CARON */ -$config['0180_024F'][] = array('upper' => 465, 'status' => 'C', 'lower' => array(466)); /* LATIN CAPITAL LETTER O WITH CARON */ -$config['0180_024F'][] = array('upper' => 467, 'status' => 'C', 'lower' => array(468)); /* LATIN CAPITAL LETTER U WITH CARON */ -$config['0180_024F'][] = array('upper' => 469, 'status' => 'C', 'lower' => array(470)); /* LATIN CAPITAL LETTER U WITH DIAERESIS AND MACRON */ -$config['0180_024F'][] = array('upper' => 471, 'status' => 'C', 'lower' => array(472)); /* LATIN CAPITAL LETTER U WITH DIAERESIS AND ACUTE */ -$config['0180_024F'][] = array('upper' => 473, 'status' => 'C', 'lower' => array(474)); /* LATIN CAPITAL LETTER U WITH DIAERESIS AND CARON */ -$config['0180_024F'][] = array('upper' => 475, 'status' => 'C', 'lower' => array(476)); /* LATIN CAPITAL LETTER U WITH DIAERESIS AND GRAVE */ -$config['0180_024F'][] = array('upper' => 478, 'status' => 'C', 'lower' => array(479)); /* LATIN CAPITAL LETTER A WITH DIAERESIS AND MACRON */ -$config['0180_024F'][] = array('upper' => 480, 'status' => 'C', 'lower' => array(481)); /* LATIN CAPITAL LETTER A WITH DOT ABOVE AND MACRON */ -$config['0180_024F'][] = array('upper' => 482, 'status' => 'C', 'lower' => array(483)); /* LATIN CAPITAL LETTER AE WITH MACRON */ -$config['0180_024F'][] = array('upper' => 484, 'status' => 'C', 'lower' => array(485)); /* LATIN CAPITAL LETTER G WITH STROKE */ -$config['0180_024F'][] = array('upper' => 486, 'status' => 'C', 'lower' => array(487)); /* LATIN CAPITAL LETTER G WITH CARON */ -$config['0180_024F'][] = array('upper' => 488, 'status' => 'C', 'lower' => array(489)); /* LATIN CAPITAL LETTER K WITH CARON */ -$config['0180_024F'][] = array('upper' => 490, 'status' => 'C', 'lower' => array(491)); /* LATIN CAPITAL LETTER O WITH OGONEK */ -$config['0180_024F'][] = array('upper' => 492, 'status' => 'C', 'lower' => array(493)); /* LATIN CAPITAL LETTER O WITH OGONEK AND MACRON */ -$config['0180_024F'][] = array('upper' => 494, 'status' => 'C', 'lower' => array(495)); /* LATIN CAPITAL LETTER EZH WITH CARON */ -$config['0180_024F'][] = array('upper' => 496, 'status' => 'F', 'lower' => array(106, 780)); /* LATIN SMALL LETTER J WITH CARON */ -$config['0180_024F'][] = array('upper' => 497, 'status' => 'C', 'lower' => array(499)); /* LATIN CAPITAL LETTER DZ */ -$config['0180_024F'][] = array('upper' => 498, 'status' => 'C', 'lower' => array(499)); /* LATIN CAPITAL LETTER D WITH SMALL LETTER Z */ -$config['0180_024F'][] = array('upper' => 500, 'status' => 'C', 'lower' => array(501)); /* LATIN CAPITAL LETTER G WITH ACUTE */ -$config['0180_024F'][] = array('upper' => 502, 'status' => 'C', 'lower' => array(405)); /* LATIN CAPITAL LETTER HWAIR */ -$config['0180_024F'][] = array('upper' => 503, 'status' => 'C', 'lower' => array(447)); /* LATIN CAPITAL LETTER WYNN */ -$config['0180_024F'][] = array('upper' => 504, 'status' => 'C', 'lower' => array(505)); /* LATIN CAPITAL LETTER N WITH GRAVE */ -$config['0180_024F'][] = array('upper' => 506, 'status' => 'C', 'lower' => array(507)); /* LATIN CAPITAL LETTER A WITH RING ABOVE AND ACUTE */ -$config['0180_024F'][] = array('upper' => 508, 'status' => 'C', 'lower' => array(509)); /* LATIN CAPITAL LETTER AE WITH ACUTE */ -$config['0180_024F'][] = array('upper' => 510, 'status' => 'C', 'lower' => array(511)); /* LATIN CAPITAL LETTER O WITH STROKE AND ACUTE */ -$config['0180_024F'][] = array('upper' => 512, 'status' => 'C', 'lower' => array(513)); /* LATIN CAPITAL LETTER A WITH DOUBLE GRAVE */ -$config['0180_024F'][] = array('upper' => 514, 'status' => 'C', 'lower' => array(515)); /* LATIN CAPITAL LETTER A WITH INVERTED BREVE */ -$config['0180_024F'][] = array('upper' => 516, 'status' => 'C', 'lower' => array(517)); /* LATIN CAPITAL LETTER E WITH DOUBLE GRAVE */ -$config['0180_024F'][] = array('upper' => 518, 'status' => 'C', 'lower' => array(519)); /* LATIN CAPITAL LETTER E WITH INVERTED BREVE */ -$config['0180_024F'][] = array('upper' => 520, 'status' => 'C', 'lower' => array(521)); /* LATIN CAPITAL LETTER I WITH DOUBLE GRAVE */ -$config['0180_024F'][] = array('upper' => 522, 'status' => 'C', 'lower' => array(523)); /* LATIN CAPITAL LETTER I WITH INVERTED BREVE */ -$config['0180_024F'][] = array('upper' => 524, 'status' => 'C', 'lower' => array(525)); /* LATIN CAPITAL LETTER O WITH DOUBLE GRAVE */ -$config['0180_024F'][] = array('upper' => 526, 'status' => 'C', 'lower' => array(527)); /* LATIN CAPITAL LETTER O WITH INVERTED BREVE */ -$config['0180_024F'][] = array('upper' => 528, 'status' => 'C', 'lower' => array(529)); /* LATIN CAPITAL LETTER R WITH DOUBLE GRAVE */ -$config['0180_024F'][] = array('upper' => 530, 'status' => 'C', 'lower' => array(531)); /* LATIN CAPITAL LETTER R WITH INVERTED BREVE */ -$config['0180_024F'][] = array('upper' => 532, 'status' => 'C', 'lower' => array(533)); /* LATIN CAPITAL LETTER U WITH DOUBLE GRAVE */ -$config['0180_024F'][] = array('upper' => 534, 'status' => 'C', 'lower' => array(535)); /* LATIN CAPITAL LETTER U WITH INVERTED BREVE */ -$config['0180_024F'][] = array('upper' => 536, 'status' => 'C', 'lower' => array(537)); /* LATIN CAPITAL LETTER S WITH COMMA BELOW */ -$config['0180_024F'][] = array('upper' => 538, 'status' => 'C', 'lower' => array(539)); /* LATIN CAPITAL LETTER T WITH COMMA BELOW */ -$config['0180_024F'][] = array('upper' => 540, 'status' => 'C', 'lower' => array(541)); /* LATIN CAPITAL LETTER YOGH */ -$config['0180_024F'][] = array('upper' => 542, 'status' => 'C', 'lower' => array(543)); /* LATIN CAPITAL LETTER H WITH CARON */ -$config['0180_024F'][] = array('upper' => 544, 'status' => 'C', 'lower' => array(414)); /* LATIN CAPITAL LETTER N WITH LONG RIGHT LEG */ -$config['0180_024F'][] = array('upper' => 546, 'status' => 'C', 'lower' => array(547)); /* LATIN CAPITAL LETTER OU */ -$config['0180_024F'][] = array('upper' => 548, 'status' => 'C', 'lower' => array(549)); /* LATIN CAPITAL LETTER Z WITH HOOK */ -$config['0180_024F'][] = array('upper' => 550, 'status' => 'C', 'lower' => array(551)); /* LATIN CAPITAL LETTER A WITH DOT ABOVE */ -$config['0180_024F'][] = array('upper' => 552, 'status' => 'C', 'lower' => array(553)); /* LATIN CAPITAL LETTER E WITH CEDILLA */ -$config['0180_024F'][] = array('upper' => 554, 'status' => 'C', 'lower' => array(555)); /* LATIN CAPITAL LETTER O WITH DIAERESIS AND MACRON */ -$config['0180_024F'][] = array('upper' => 556, 'status' => 'C', 'lower' => array(557)); /* LATIN CAPITAL LETTER O WITH TILDE AND MACRON */ -$config['0180_024F'][] = array('upper' => 558, 'status' => 'C', 'lower' => array(559)); /* LATIN CAPITAL LETTER O WITH DOT ABOVE */ -$config['0180_024F'][] = array('upper' => 560, 'status' => 'C', 'lower' => array(561)); /* LATIN CAPITAL LETTER O WITH DOT ABOVE AND MACRON */ -$config['0180_024F'][] = array('upper' => 562, 'status' => 'C', 'lower' => array(563)); /* LATIN CAPITAL LETTER Y WITH MACRON */ -$config['0180_024F'][] = array('upper' => 570, 'status' => 'C', 'lower' => array(11365)); /* LATIN CAPITAL LETTER A WITH STROKE */ -$config['0180_024F'][] = array('upper' => 571, 'status' => 'C', 'lower' => array(572)); /* LATIN CAPITAL LETTER C WITH STROKE */ -$config['0180_024F'][] = array('upper' => 573, 'status' => 'C', 'lower' => array(410)); /* LATIN CAPITAL LETTER L WITH BAR */ -$config['0180_024F'][] = array('upper' => 574, 'status' => 'C', 'lower' => array(11366)); /* LATIN CAPITAL LETTER T WITH DIAGONAL STROKE */ -$config['0180_024F'][] = array('upper' => 577, 'status' => 'C', 'lower' => array(578)); /* LATIN CAPITAL LETTER GLOTTAL STOP */ -$config['0180_024F'][] = array('upper' => 579, 'status' => 'C', 'lower' => array(384)); /* LATIN CAPITAL LETTER B WITH STROKE */ -$config['0180_024F'][] = array('upper' => 580, 'status' => 'C', 'lower' => array(649)); /* LATIN CAPITAL LETTER U BAR */ -$config['0180_024F'][] = array('upper' => 581, 'status' => 'C', 'lower' => array(652)); /* LATIN CAPITAL LETTER TURNED V */ -$config['0180_024F'][] = array('upper' => 582, 'status' => 'C', 'lower' => array(583)); /* LATIN CAPITAL LETTER E WITH STROKE */ -$config['0180_024F'][] = array('upper' => 584, 'status' => 'C', 'lower' => array(585)); /* LATIN CAPITAL LETTER J WITH STROKE */ -$config['0180_024F'][] = array('upper' => 586, 'status' => 'C', 'lower' => array(587)); /* LATIN CAPITAL LETTER SMALL Q WITH HOOK TAIL */ -$config['0180_024F'][] = array('upper' => 588, 'status' => 'C', 'lower' => array(589)); /* LATIN CAPITAL LETTER R WITH STROKE */ -$config['0180_024F'][] = array('upper' => 590, 'status' => 'C', 'lower' => array(591)); /* LATIN CAPITAL LETTER Y WITH STROKE */ +$config['0180_024F'][] = ['upper' => 385, 'status' => 'C', 'lower' => [595]]; /* LATIN CAPITAL LETTER B WITH HOOK */ +$config['0180_024F'][] = ['upper' => 386, 'status' => 'C', 'lower' => [387]]; /* LATIN CAPITAL LETTER B WITH TOPBAR */ +$config['0180_024F'][] = ['upper' => 388, 'status' => 'C', 'lower' => [389]]; /* LATIN CAPITAL LETTER TONE SIX */ +$config['0180_024F'][] = ['upper' => 390, 'status' => 'C', 'lower' => [596]]; /* LATIN CAPITAL LETTER OPEN O */ +$config['0180_024F'][] = ['upper' => 391, 'status' => 'C', 'lower' => [392]]; /* LATIN CAPITAL LETTER C WITH HOOK */ +$config['0180_024F'][] = ['upper' => 393, 'status' => 'C', 'lower' => [598]]; /* LATIN CAPITAL LETTER AFRICAN D */ +$config['0180_024F'][] = ['upper' => 394, 'status' => 'C', 'lower' => [599]]; /* LATIN CAPITAL LETTER D WITH HOOK */ +$config['0180_024F'][] = ['upper' => 395, 'status' => 'C', 'lower' => [396]]; /* LATIN CAPITAL LETTER D WITH TOPBAR */ +$config['0180_024F'][] = ['upper' => 398, 'status' => 'C', 'lower' => [477]]; /* LATIN CAPITAL LETTER REVERSED E */ +$config['0180_024F'][] = ['upper' => 399, 'status' => 'C', 'lower' => [601]]; /* LATIN CAPITAL LETTER SCHWA */ +$config['0180_024F'][] = ['upper' => 400, 'status' => 'C', 'lower' => [603]]; /* LATIN CAPITAL LETTER OPEN E */ +$config['0180_024F'][] = ['upper' => 401, 'status' => 'C', 'lower' => [402]]; /* LATIN CAPITAL LETTER F WITH HOOK */ +$config['0180_024F'][] = ['upper' => 403, 'status' => 'C', 'lower' => [608]]; /* LATIN CAPITAL LETTER G WITH HOOK */ +$config['0180_024F'][] = ['upper' => 404, 'status' => 'C', 'lower' => [611]]; /* LATIN CAPITAL LETTER GAMMA */ +$config['0180_024F'][] = ['upper' => 406, 'status' => 'C', 'lower' => [617]]; /* LATIN CAPITAL LETTER IOTA */ +$config['0180_024F'][] = ['upper' => 407, 'status' => 'C', 'lower' => [616]]; /* LATIN CAPITAL LETTER I WITH STROKE */ +$config['0180_024F'][] = ['upper' => 408, 'status' => 'C', 'lower' => [409]]; /* LATIN CAPITAL LETTER K WITH HOOK */ +$config['0180_024F'][] = ['upper' => 412, 'status' => 'C', 'lower' => [623]]; /* LATIN CAPITAL LETTER TURNED M */ +$config['0180_024F'][] = ['upper' => 413, 'status' => 'C', 'lower' => [626]]; /* LATIN CAPITAL LETTER N WITH LEFT HOOK */ +$config['0180_024F'][] = ['upper' => 415, 'status' => 'C', 'lower' => [629]]; /* LATIN CAPITAL LETTER O WITH MIDDLE TILDE */ +$config['0180_024F'][] = ['upper' => 416, 'status' => 'C', 'lower' => [417]]; /* LATIN CAPITAL LETTER O WITH HORN */ +$config['0180_024F'][] = ['upper' => 418, 'status' => 'C', 'lower' => [419]]; /* LATIN CAPITAL LETTER OI */ +$config['0180_024F'][] = ['upper' => 420, 'status' => 'C', 'lower' => [421]]; /* LATIN CAPITAL LETTER P WITH HOOK */ +$config['0180_024F'][] = ['upper' => 422, 'status' => 'C', 'lower' => [640]]; /* LATIN LETTER YR */ +$config['0180_024F'][] = ['upper' => 423, 'status' => 'C', 'lower' => [424]]; /* LATIN CAPITAL LETTER TONE TWO */ +$config['0180_024F'][] = ['upper' => 425, 'status' => 'C', 'lower' => [643]]; /* LATIN CAPITAL LETTER ESH */ +$config['0180_024F'][] = ['upper' => 428, 'status' => 'C', 'lower' => [429]]; /* LATIN CAPITAL LETTER T WITH HOOK */ +$config['0180_024F'][] = ['upper' => 430, 'status' => 'C', 'lower' => [648]]; /* LATIN CAPITAL LETTER T WITH RETROFLEX HOOK */ +$config['0180_024F'][] = ['upper' => 431, 'status' => 'C', 'lower' => [432]]; /* LATIN CAPITAL LETTER U WITH HORN */ +$config['0180_024F'][] = ['upper' => 433, 'status' => 'C', 'lower' => [650]]; /* LATIN CAPITAL LETTER UPSILON */ +$config['0180_024F'][] = ['upper' => 434, 'status' => 'C', 'lower' => [651]]; /* LATIN CAPITAL LETTER V WITH HOOK */ +$config['0180_024F'][] = ['upper' => 435, 'status' => 'C', 'lower' => [436]]; /* LATIN CAPITAL LETTER Y WITH HOOK */ +$config['0180_024F'][] = ['upper' => 437, 'status' => 'C', 'lower' => [438]]; /* LATIN CAPITAL LETTER Z WITH STROKE */ +$config['0180_024F'][] = ['upper' => 439, 'status' => 'C', 'lower' => [658]]; /* LATIN CAPITAL LETTER EZH */ +$config['0180_024F'][] = ['upper' => 440, 'status' => 'C', 'lower' => [441]]; /* LATIN CAPITAL LETTER EZH REVERSED */ +$config['0180_024F'][] = ['upper' => 444, 'status' => 'C', 'lower' => [445]]; /* LATIN CAPITAL LETTER TONE FIVE */ +$config['0180_024F'][] = ['upper' => 452, 'status' => 'C', 'lower' => [454]]; /* LATIN CAPITAL LETTER DZ WITH CARON */ +$config['0180_024F'][] = ['upper' => 453, 'status' => 'C', 'lower' => [454]]; /* LATIN CAPITAL LETTER D WITH SMALL LETTER Z WITH CARON */ +$config['0180_024F'][] = ['upper' => 455, 'status' => 'C', 'lower' => [457]]; /* LATIN CAPITAL LETTER LJ */ +$config['0180_024F'][] = ['upper' => 456, 'status' => 'C', 'lower' => [457]]; /* LATIN CAPITAL LETTER L WITH SMALL LETTER J */ +$config['0180_024F'][] = ['upper' => 458, 'status' => 'C', 'lower' => [460]]; /* LATIN CAPITAL LETTER NJ */ +$config['0180_024F'][] = ['upper' => 459, 'status' => 'C', 'lower' => [460]]; /* LATIN CAPITAL LETTER N WITH SMALL LETTER J */ +$config['0180_024F'][] = ['upper' => 461, 'status' => 'C', 'lower' => [462]]; /* LATIN CAPITAL LETTER A WITH CARON */ +$config['0180_024F'][] = ['upper' => 463, 'status' => 'C', 'lower' => [464]]; /* LATIN CAPITAL LETTER I WITH CARON */ +$config['0180_024F'][] = ['upper' => 465, 'status' => 'C', 'lower' => [466]]; /* LATIN CAPITAL LETTER O WITH CARON */ +$config['0180_024F'][] = ['upper' => 467, 'status' => 'C', 'lower' => [468]]; /* LATIN CAPITAL LETTER U WITH CARON */ +$config['0180_024F'][] = ['upper' => 469, 'status' => 'C', 'lower' => [470]]; /* LATIN CAPITAL LETTER U WITH DIAERESIS AND MACRON */ +$config['0180_024F'][] = ['upper' => 471, 'status' => 'C', 'lower' => [472]]; /* LATIN CAPITAL LETTER U WITH DIAERESIS AND ACUTE */ +$config['0180_024F'][] = ['upper' => 473, 'status' => 'C', 'lower' => [474]]; /* LATIN CAPITAL LETTER U WITH DIAERESIS AND CARON */ +$config['0180_024F'][] = ['upper' => 475, 'status' => 'C', 'lower' => [476]]; /* LATIN CAPITAL LETTER U WITH DIAERESIS AND GRAVE */ +$config['0180_024F'][] = ['upper' => 478, 'status' => 'C', 'lower' => [479]]; /* LATIN CAPITAL LETTER A WITH DIAERESIS AND MACRON */ +$config['0180_024F'][] = ['upper' => 480, 'status' => 'C', 'lower' => [481]]; /* LATIN CAPITAL LETTER A WITH DOT ABOVE AND MACRON */ +$config['0180_024F'][] = ['upper' => 482, 'status' => 'C', 'lower' => [483]]; /* LATIN CAPITAL LETTER AE WITH MACRON */ +$config['0180_024F'][] = ['upper' => 484, 'status' => 'C', 'lower' => [485]]; /* LATIN CAPITAL LETTER G WITH STROKE */ +$config['0180_024F'][] = ['upper' => 486, 'status' => 'C', 'lower' => [487]]; /* LATIN CAPITAL LETTER G WITH CARON */ +$config['0180_024F'][] = ['upper' => 488, 'status' => 'C', 'lower' => [489]]; /* LATIN CAPITAL LETTER K WITH CARON */ +$config['0180_024F'][] = ['upper' => 490, 'status' => 'C', 'lower' => [491]]; /* LATIN CAPITAL LETTER O WITH OGONEK */ +$config['0180_024F'][] = ['upper' => 492, 'status' => 'C', 'lower' => [493]]; /* LATIN CAPITAL LETTER O WITH OGONEK AND MACRON */ +$config['0180_024F'][] = ['upper' => 494, 'status' => 'C', 'lower' => [495]]; /* LATIN CAPITAL LETTER EZH WITH CARON */ +$config['0180_024F'][] = ['upper' => 496, 'status' => 'F', 'lower' => [106, 780]]; /* LATIN SMALL LETTER J WITH CARON */ +$config['0180_024F'][] = ['upper' => 497, 'status' => 'C', 'lower' => [499]]; /* LATIN CAPITAL LETTER DZ */ +$config['0180_024F'][] = ['upper' => 498, 'status' => 'C', 'lower' => [499]]; /* LATIN CAPITAL LETTER D WITH SMALL LETTER Z */ +$config['0180_024F'][] = ['upper' => 500, 'status' => 'C', 'lower' => [501]]; /* LATIN CAPITAL LETTER G WITH ACUTE */ +$config['0180_024F'][] = ['upper' => 502, 'status' => 'C', 'lower' => [405]]; /* LATIN CAPITAL LETTER HWAIR */ +$config['0180_024F'][] = ['upper' => 503, 'status' => 'C', 'lower' => [447]]; /* LATIN CAPITAL LETTER WYNN */ +$config['0180_024F'][] = ['upper' => 504, 'status' => 'C', 'lower' => [505]]; /* LATIN CAPITAL LETTER N WITH GRAVE */ +$config['0180_024F'][] = ['upper' => 506, 'status' => 'C', 'lower' => [507]]; /* LATIN CAPITAL LETTER A WITH RING ABOVE AND ACUTE */ +$config['0180_024F'][] = ['upper' => 508, 'status' => 'C', 'lower' => [509]]; /* LATIN CAPITAL LETTER AE WITH ACUTE */ +$config['0180_024F'][] = ['upper' => 510, 'status' => 'C', 'lower' => [511]]; /* LATIN CAPITAL LETTER O WITH STROKE AND ACUTE */ +$config['0180_024F'][] = ['upper' => 512, 'status' => 'C', 'lower' => [513]]; /* LATIN CAPITAL LETTER A WITH DOUBLE GRAVE */ +$config['0180_024F'][] = ['upper' => 514, 'status' => 'C', 'lower' => [515]]; /* LATIN CAPITAL LETTER A WITH INVERTED BREVE */ +$config['0180_024F'][] = ['upper' => 516, 'status' => 'C', 'lower' => [517]]; /* LATIN CAPITAL LETTER E WITH DOUBLE GRAVE */ +$config['0180_024F'][] = ['upper' => 518, 'status' => 'C', 'lower' => [519]]; /* LATIN CAPITAL LETTER E WITH INVERTED BREVE */ +$config['0180_024F'][] = ['upper' => 520, 'status' => 'C', 'lower' => [521]]; /* LATIN CAPITAL LETTER I WITH DOUBLE GRAVE */ +$config['0180_024F'][] = ['upper' => 522, 'status' => 'C', 'lower' => [523]]; /* LATIN CAPITAL LETTER I WITH INVERTED BREVE */ +$config['0180_024F'][] = ['upper' => 524, 'status' => 'C', 'lower' => [525]]; /* LATIN CAPITAL LETTER O WITH DOUBLE GRAVE */ +$config['0180_024F'][] = ['upper' => 526, 'status' => 'C', 'lower' => [527]]; /* LATIN CAPITAL LETTER O WITH INVERTED BREVE */ +$config['0180_024F'][] = ['upper' => 528, 'status' => 'C', 'lower' => [529]]; /* LATIN CAPITAL LETTER R WITH DOUBLE GRAVE */ +$config['0180_024F'][] = ['upper' => 530, 'status' => 'C', 'lower' => [531]]; /* LATIN CAPITAL LETTER R WITH INVERTED BREVE */ +$config['0180_024F'][] = ['upper' => 532, 'status' => 'C', 'lower' => [533]]; /* LATIN CAPITAL LETTER U WITH DOUBLE GRAVE */ +$config['0180_024F'][] = ['upper' => 534, 'status' => 'C', 'lower' => [535]]; /* LATIN CAPITAL LETTER U WITH INVERTED BREVE */ +$config['0180_024F'][] = ['upper' => 536, 'status' => 'C', 'lower' => [537]]; /* LATIN CAPITAL LETTER S WITH COMMA BELOW */ +$config['0180_024F'][] = ['upper' => 538, 'status' => 'C', 'lower' => [539]]; /* LATIN CAPITAL LETTER T WITH COMMA BELOW */ +$config['0180_024F'][] = ['upper' => 540, 'status' => 'C', 'lower' => [541]]; /* LATIN CAPITAL LETTER YOGH */ +$config['0180_024F'][] = ['upper' => 542, 'status' => 'C', 'lower' => [543]]; /* LATIN CAPITAL LETTER H WITH CARON */ +$config['0180_024F'][] = ['upper' => 544, 'status' => 'C', 'lower' => [414]]; /* LATIN CAPITAL LETTER N WITH LONG RIGHT LEG */ +$config['0180_024F'][] = ['upper' => 546, 'status' => 'C', 'lower' => [547]]; /* LATIN CAPITAL LETTER OU */ +$config['0180_024F'][] = ['upper' => 548, 'status' => 'C', 'lower' => [549]]; /* LATIN CAPITAL LETTER Z WITH HOOK */ +$config['0180_024F'][] = ['upper' => 550, 'status' => 'C', 'lower' => [551]]; /* LATIN CAPITAL LETTER A WITH DOT ABOVE */ +$config['0180_024F'][] = ['upper' => 552, 'status' => 'C', 'lower' => [553]]; /* LATIN CAPITAL LETTER E WITH CEDILLA */ +$config['0180_024F'][] = ['upper' => 554, 'status' => 'C', 'lower' => [555]]; /* LATIN CAPITAL LETTER O WITH DIAERESIS AND MACRON */ +$config['0180_024F'][] = ['upper' => 556, 'status' => 'C', 'lower' => [557]]; /* LATIN CAPITAL LETTER O WITH TILDE AND MACRON */ +$config['0180_024F'][] = ['upper' => 558, 'status' => 'C', 'lower' => [559]]; /* LATIN CAPITAL LETTER O WITH DOT ABOVE */ +$config['0180_024F'][] = ['upper' => 560, 'status' => 'C', 'lower' => [561]]; /* LATIN CAPITAL LETTER O WITH DOT ABOVE AND MACRON */ +$config['0180_024F'][] = ['upper' => 562, 'status' => 'C', 'lower' => [563]]; /* LATIN CAPITAL LETTER Y WITH MACRON */ +$config['0180_024F'][] = ['upper' => 570, 'status' => 'C', 'lower' => [11365]]; /* LATIN CAPITAL LETTER A WITH STROKE */ +$config['0180_024F'][] = ['upper' => 571, 'status' => 'C', 'lower' => [572]]; /* LATIN CAPITAL LETTER C WITH STROKE */ +$config['0180_024F'][] = ['upper' => 573, 'status' => 'C', 'lower' => [410]]; /* LATIN CAPITAL LETTER L WITH BAR */ +$config['0180_024F'][] = ['upper' => 574, 'status' => 'C', 'lower' => [11366]]; /* LATIN CAPITAL LETTER T WITH DIAGONAL STROKE */ +$config['0180_024F'][] = ['upper' => 577, 'status' => 'C', 'lower' => [578]]; /* LATIN CAPITAL LETTER GLOTTAL STOP */ +$config['0180_024F'][] = ['upper' => 579, 'status' => 'C', 'lower' => [384]]; /* LATIN CAPITAL LETTER B WITH STROKE */ +$config['0180_024F'][] = ['upper' => 580, 'status' => 'C', 'lower' => [649]]; /* LATIN CAPITAL LETTER U BAR */ +$config['0180_024F'][] = ['upper' => 581, 'status' => 'C', 'lower' => [652]]; /* LATIN CAPITAL LETTER TURNED V */ +$config['0180_024F'][] = ['upper' => 582, 'status' => 'C', 'lower' => [583]]; /* LATIN CAPITAL LETTER E WITH STROKE */ +$config['0180_024F'][] = ['upper' => 584, 'status' => 'C', 'lower' => [585]]; /* LATIN CAPITAL LETTER J WITH STROKE */ +$config['0180_024F'][] = ['upper' => 586, 'status' => 'C', 'lower' => [587]]; /* LATIN CAPITAL LETTER SMALL Q WITH HOOK TAIL */ +$config['0180_024F'][] = ['upper' => 588, 'status' => 'C', 'lower' => [589]]; /* LATIN CAPITAL LETTER R WITH STROKE */ +$config['0180_024F'][] = ['upper' => 590, 'status' => 'C', 'lower' => [591]]; /* LATIN CAPITAL LETTER Y WITH STROKE */ diff --git a/lib/Cake/Config/unicode/casefolding/0250_02af.php b/lib/Cake/Config/unicode/casefolding/0250_02af.php index b9e09a8c..65a70af6 100755 --- a/lib/Cake/Config/unicode/casefolding/0250_02af.php +++ b/lib/Cake/Config/unicode/casefolding/0250_02af.php @@ -27,7 +27,7 @@ * * The lower filed is an array of the decimal values that form the lower case version of a character. * - * The status field is: + * The status field is: * C: common case folding, common mappings shared by both simple and full mappings. * F: full case folding, mappings that cause strings to grow in length. Multiple characters are separated by spaces. * S: simple case folding, mappings to single characters where different from F. @@ -37,4 +37,4 @@ * Note that the Turkic mappings do not maintain canonical equivalence without additional processing. * See the discussions of case mapping in the Unicode Standard for more information. */ -$config['0250_02af'][] = array('upper' => 422, 'status' => 'C', 'lower' => array(640)); +$config['0250_02af'][] = ['upper' => 422, 'status' => 'C', 'lower' => [640]]; diff --git a/lib/Cake/Config/unicode/casefolding/0370_03ff.php b/lib/Cake/Config/unicode/casefolding/0370_03ff.php index 97eac644..9b7ccdd6 100755 --- a/lib/Cake/Config/unicode/casefolding/0370_03ff.php +++ b/lib/Cake/Config/unicode/casefolding/0370_03ff.php @@ -27,7 +27,7 @@ * * The lower filed is an array of the decimal values that form the lower case version of a character. * - * The status field is: + * The status field is: * C: common case folding, common mappings shared by both simple and full mappings. * F: full case folding, mappings that cause strings to grow in length. Multiple characters are separated by spaces. * S: simple case folding, mappings to single characters where different from F. @@ -37,65 +37,65 @@ * Note that the Turkic mappings do not maintain canonical equivalence without additional processing. * See the discussions of case mapping in the Unicode Standard for more information. */ -$config['0370_03ff'][] = array('upper' => 902, 'status' => 'C', 'lower' => array(940)); /* GREEK CAPITAL LETTER ALPHA WITH TONOS */ -$config['0370_03ff'][] = array('upper' => 904, 'status' => 'C', 'lower' => array(941)); /* GREEK CAPITAL LETTER EPSILON WITH TONOS */ -$config['0370_03ff'][] = array('upper' => 905, 'status' => 'C', 'lower' => array(942)); /* GREEK CAPITAL LETTER ETA WITH TONOS */ -$config['0370_03ff'][] = array('upper' => 906, 'status' => 'C', 'lower' => array(943)); /* GREEK CAPITAL LETTER IOTA WITH TONOS */ -$config['0370_03ff'][] = array('upper' => 908, 'status' => 'C', 'lower' => array(972)); /* GREEK CAPITAL LETTER OMICRON WITH TONOS */ -$config['0370_03ff'][] = array('upper' => 910, 'status' => 'C', 'lower' => array(973)); /* GREEK CAPITAL LETTER UPSILON WITH TONOS */ -$config['0370_03ff'][] = array('upper' => 911, 'status' => 'C', 'lower' => array(974)); /* GREEK CAPITAL LETTER OMEGA WITH TONOS */ +$config['0370_03ff'][] = ['upper' => 902, 'status' => 'C', 'lower' => [940]]; /* GREEK CAPITAL LETTER ALPHA WITH TONOS */ +$config['0370_03ff'][] = ['upper' => 904, 'status' => 'C', 'lower' => [941]]; /* GREEK CAPITAL LETTER EPSILON WITH TONOS */ +$config['0370_03ff'][] = ['upper' => 905, 'status' => 'C', 'lower' => [942]]; /* GREEK CAPITAL LETTER ETA WITH TONOS */ +$config['0370_03ff'][] = ['upper' => 906, 'status' => 'C', 'lower' => [943]]; /* GREEK CAPITAL LETTER IOTA WITH TONOS */ +$config['0370_03ff'][] = ['upper' => 908, 'status' => 'C', 'lower' => [972]]; /* GREEK CAPITAL LETTER OMICRON WITH TONOS */ +$config['0370_03ff'][] = ['upper' => 910, 'status' => 'C', 'lower' => [973]]; /* GREEK CAPITAL LETTER UPSILON WITH TONOS */ +$config['0370_03ff'][] = ['upper' => 911, 'status' => 'C', 'lower' => [974]]; /* GREEK CAPITAL LETTER OMEGA WITH TONOS */ //$config['0370_03ff'][] = array('upper' => 912, 'status' => 'F', 'lower' => array(953, 776, 769)); /* GREEK SMALL LETTER IOTA WITH DIALYTIKA AND TONOS */ -$config['0370_03ff'][] = array('upper' => 913, 'status' => 'C', 'lower' => array(945)); /* GREEK CAPITAL LETTER ALPHA */ -$config['0370_03ff'][] = array('upper' => 914, 'status' => 'C', 'lower' => array(946)); /* GREEK CAPITAL LETTER BETA */ -$config['0370_03ff'][] = array('upper' => 915, 'status' => 'C', 'lower' => array(947)); /* GREEK CAPITAL LETTER GAMMA */ -$config['0370_03ff'][] = array('upper' => 916, 'status' => 'C', 'lower' => array(948)); /* GREEK CAPITAL LETTER DELTA */ -$config['0370_03ff'][] = array('upper' => 917, 'status' => 'C', 'lower' => array(949)); /* GREEK CAPITAL LETTER EPSILON */ -$config['0370_03ff'][] = array('upper' => 918, 'status' => 'C', 'lower' => array(950)); /* GREEK CAPITAL LETTER ZETA */ -$config['0370_03ff'][] = array('upper' => 919, 'status' => 'C', 'lower' => array(951)); /* GREEK CAPITAL LETTER ETA */ -$config['0370_03ff'][] = array('upper' => 920, 'status' => 'C', 'lower' => array(952)); /* GREEK CAPITAL LETTER THETA */ -$config['0370_03ff'][] = array('upper' => 921, 'status' => 'C', 'lower' => array(953)); /* GREEK CAPITAL LETTER IOTA */ -$config['0370_03ff'][] = array('upper' => 922, 'status' => 'C', 'lower' => array(954)); /* GREEK CAPITAL LETTER KAPPA */ -$config['0370_03ff'][] = array('upper' => 923, 'status' => 'C', 'lower' => array(955)); /* GREEK CAPITAL LETTER LAMDA */ -$config['0370_03ff'][] = array('upper' => 924, 'status' => 'C', 'lower' => array(956)); /* GREEK CAPITAL LETTER MU */ -$config['0370_03ff'][] = array('upper' => 925, 'status' => 'C', 'lower' => array(957)); /* GREEK CAPITAL LETTER NU */ -$config['0370_03ff'][] = array('upper' => 926, 'status' => 'C', 'lower' => array(958)); /* GREEK CAPITAL LETTER XI */ -$config['0370_03ff'][] = array('upper' => 927, 'status' => 'C', 'lower' => array(959)); /* GREEK CAPITAL LETTER OMICRON */ -$config['0370_03ff'][] = array('upper' => 928, 'status' => 'C', 'lower' => array(960)); /* GREEK CAPITAL LETTER PI */ -$config['0370_03ff'][] = array('upper' => 929, 'status' => 'C', 'lower' => array(961)); /* GREEK CAPITAL LETTER RHO */ -$config['0370_03ff'][] = array('upper' => 931, 'status' => 'C', 'lower' => array(963)); /* GREEK CAPITAL LETTER SIGMA */ -$config['0370_03ff'][] = array('upper' => 932, 'status' => 'C', 'lower' => array(964)); /* GREEK CAPITAL LETTER TAU */ -$config['0370_03ff'][] = array('upper' => 933, 'status' => 'C', 'lower' => array(965)); /* GREEK CAPITAL LETTER UPSILON */ -$config['0370_03ff'][] = array('upper' => 934, 'status' => 'C', 'lower' => array(966)); /* GREEK CAPITAL LETTER PHI */ -$config['0370_03ff'][] = array('upper' => 935, 'status' => 'C', 'lower' => array(967)); /* GREEK CAPITAL LETTER CHI */ -$config['0370_03ff'][] = array('upper' => 936, 'status' => 'C', 'lower' => array(968)); /* GREEK CAPITAL LETTER PSI */ -$config['0370_03ff'][] = array('upper' => 937, 'status' => 'C', 'lower' => array(969)); /* GREEK CAPITAL LETTER OMEGA */ -$config['0370_03ff'][] = array('upper' => 938, 'status' => 'C', 'lower' => array(970)); /* GREEK CAPITAL LETTER IOTA WITH DIALYTIKA */ -$config['0370_03ff'][] = array('upper' => 939, 'status' => 'C', 'lower' => array(971)); /* GREEK CAPITAL LETTER UPSILON WITH DIALYTIKA */ -$config['0370_03ff'][] = array('upper' => 944, 'status' => 'F', 'lower' => array(965, 776, 769)); /* GREEK SMALL LETTER UPSILON WITH DIALYTIKA AND TONOS */ -$config['0370_03ff'][] = array('upper' => 962, 'status' => 'C', 'lower' => array(963)); /* GREEK SMALL LETTER FINAL SIGMA */ -$config['0370_03ff'][] = array('upper' => 976, 'status' => 'C', 'lower' => array(946)); /* GREEK BETA SYMBOL */ -$config['0370_03ff'][] = array('upper' => 977, 'status' => 'C', 'lower' => array(952)); /* GREEK THETA SYMBOL */ -$config['0370_03ff'][] = array('upper' => 981, 'status' => 'C', 'lower' => array(966)); /* GREEK PHI SYMBOL */ -$config['0370_03ff'][] = array('upper' => 982, 'status' => 'C', 'lower' => array(960)); /* GREEK PI SYMBOL */ -$config['0370_03ff'][] = array('upper' => 984, 'status' => 'C', 'lower' => array(985)); /* GREEK LETTER ARCHAIC KOPPA */ -$config['0370_03ff'][] = array('upper' => 986, 'status' => 'C', 'lower' => array(987)); /* GREEK LETTER STIGMA */ -$config['0370_03ff'][] = array('upper' => 988, 'status' => 'C', 'lower' => array(989)); /* GREEK LETTER DIGAMMA */ -$config['0370_03ff'][] = array('upper' => 990, 'status' => 'C', 'lower' => array(991)); /* GREEK LETTER KOPPA */ -$config['0370_03ff'][] = array('upper' => 992, 'status' => 'C', 'lower' => array(993)); /* GREEK LETTER SAMPI */ -$config['0370_03ff'][] = array('upper' => 994, 'status' => 'C', 'lower' => array(995)); /* COPTIC CAPITAL LETTER SHEI */ -$config['0370_03ff'][] = array('upper' => 996, 'status' => 'C', 'lower' => array(997)); /* COPTIC CAPITAL LETTER FEI */ -$config['0370_03ff'][] = array('upper' => 998, 'status' => 'C', 'lower' => array(999)); /* COPTIC CAPITAL LETTER KHEI */ -$config['0370_03ff'][] = array('upper' => 1000, 'status' => 'C', 'lower' => array(1001)); /* COPTIC CAPITAL LETTER HORI */ -$config['0370_03ff'][] = array('upper' => 1002, 'status' => 'C', 'lower' => array(1003)); /* COPTIC CAPITAL LETTER GANGIA */ -$config['0370_03ff'][] = array('upper' => 1004, 'status' => 'C', 'lower' => array(1005)); /* COPTIC CAPITAL LETTER SHIMA */ -$config['0370_03ff'][] = array('upper' => 1006, 'status' => 'C', 'lower' => array(1007)); /* COPTIC CAPITAL LETTER DEI */ -$config['0370_03ff'][] = array('upper' => 1008, 'status' => 'C', 'lower' => array(954)); /* GREEK KAPPA SYMBOL */ -$config['0370_03ff'][] = array('upper' => 1009, 'status' => 'C', 'lower' => array(961)); /* GREEK RHO SYMBOL */ -$config['0370_03ff'][] = array('upper' => 1012, 'status' => 'C', 'lower' => array(952)); /* GREEK CAPITAL THETA SYMBOL */ -$config['0370_03ff'][] = array('upper' => 1013, 'status' => 'C', 'lower' => array(949)); /* GREEK LUNATE EPSILON SYMBOL */ -$config['0370_03ff'][] = array('upper' => 1015, 'status' => 'C', 'lower' => array(1016)); /* GREEK CAPITAL LETTER SHO */ -$config['0370_03ff'][] = array('upper' => 1017, 'status' => 'C', 'lower' => array(1010)); /* GREEK CAPITAL LUNATE SIGMA SYMBOL */ -$config['0370_03ff'][] = array('upper' => 1018, 'status' => 'C', 'lower' => array(1019)); /* GREEK CAPITAL LETTER SAN */ -$config['0370_03ff'][] = array('upper' => 1021, 'status' => 'C', 'lower' => array(891)); /* GREEK CAPITAL REVERSED LUNATE SIGMA SYMBOL */ -$config['0370_03ff'][] = array('upper' => 1022, 'status' => 'C', 'lower' => array(892)); /* GREEK CAPITAL DOTTED LUNATE SIGMA SYMBOL */ -$config['0370_03ff'][] = array('upper' => 1023, 'status' => 'C', 'lower' => array(893)); /* GREEK CAPITAL REVERSED DOTTED LUNATE SIGMA SYMBOL */ +$config['0370_03ff'][] = ['upper' => 913, 'status' => 'C', 'lower' => [945]]; /* GREEK CAPITAL LETTER ALPHA */ +$config['0370_03ff'][] = ['upper' => 914, 'status' => 'C', 'lower' => [946]]; /* GREEK CAPITAL LETTER BETA */ +$config['0370_03ff'][] = ['upper' => 915, 'status' => 'C', 'lower' => [947]]; /* GREEK CAPITAL LETTER GAMMA */ +$config['0370_03ff'][] = ['upper' => 916, 'status' => 'C', 'lower' => [948]]; /* GREEK CAPITAL LETTER DELTA */ +$config['0370_03ff'][] = ['upper' => 917, 'status' => 'C', 'lower' => [949]]; /* GREEK CAPITAL LETTER EPSILON */ +$config['0370_03ff'][] = ['upper' => 918, 'status' => 'C', 'lower' => [950]]; /* GREEK CAPITAL LETTER ZETA */ +$config['0370_03ff'][] = ['upper' => 919, 'status' => 'C', 'lower' => [951]]; /* GREEK CAPITAL LETTER ETA */ +$config['0370_03ff'][] = ['upper' => 920, 'status' => 'C', 'lower' => [952]]; /* GREEK CAPITAL LETTER THETA */ +$config['0370_03ff'][] = ['upper' => 921, 'status' => 'C', 'lower' => [953]]; /* GREEK CAPITAL LETTER IOTA */ +$config['0370_03ff'][] = ['upper' => 922, 'status' => 'C', 'lower' => [954]]; /* GREEK CAPITAL LETTER KAPPA */ +$config['0370_03ff'][] = ['upper' => 923, 'status' => 'C', 'lower' => [955]]; /* GREEK CAPITAL LETTER LAMDA */ +$config['0370_03ff'][] = ['upper' => 924, 'status' => 'C', 'lower' => [956]]; /* GREEK CAPITAL LETTER MU */ +$config['0370_03ff'][] = ['upper' => 925, 'status' => 'C', 'lower' => [957]]; /* GREEK CAPITAL LETTER NU */ +$config['0370_03ff'][] = ['upper' => 926, 'status' => 'C', 'lower' => [958]]; /* GREEK CAPITAL LETTER XI */ +$config['0370_03ff'][] = ['upper' => 927, 'status' => 'C', 'lower' => [959]]; /* GREEK CAPITAL LETTER OMICRON */ +$config['0370_03ff'][] = ['upper' => 928, 'status' => 'C', 'lower' => [960]]; /* GREEK CAPITAL LETTER PI */ +$config['0370_03ff'][] = ['upper' => 929, 'status' => 'C', 'lower' => [961]]; /* GREEK CAPITAL LETTER RHO */ +$config['0370_03ff'][] = ['upper' => 931, 'status' => 'C', 'lower' => [963]]; /* GREEK CAPITAL LETTER SIGMA */ +$config['0370_03ff'][] = ['upper' => 932, 'status' => 'C', 'lower' => [964]]; /* GREEK CAPITAL LETTER TAU */ +$config['0370_03ff'][] = ['upper' => 933, 'status' => 'C', 'lower' => [965]]; /* GREEK CAPITAL LETTER UPSILON */ +$config['0370_03ff'][] = ['upper' => 934, 'status' => 'C', 'lower' => [966]]; /* GREEK CAPITAL LETTER PHI */ +$config['0370_03ff'][] = ['upper' => 935, 'status' => 'C', 'lower' => [967]]; /* GREEK CAPITAL LETTER CHI */ +$config['0370_03ff'][] = ['upper' => 936, 'status' => 'C', 'lower' => [968]]; /* GREEK CAPITAL LETTER PSI */ +$config['0370_03ff'][] = ['upper' => 937, 'status' => 'C', 'lower' => [969]]; /* GREEK CAPITAL LETTER OMEGA */ +$config['0370_03ff'][] = ['upper' => 938, 'status' => 'C', 'lower' => [970]]; /* GREEK CAPITAL LETTER IOTA WITH DIALYTIKA */ +$config['0370_03ff'][] = ['upper' => 939, 'status' => 'C', 'lower' => [971]]; /* GREEK CAPITAL LETTER UPSILON WITH DIALYTIKA */ +$config['0370_03ff'][] = ['upper' => 944, 'status' => 'F', 'lower' => [965, 776, 769]]; /* GREEK SMALL LETTER UPSILON WITH DIALYTIKA AND TONOS */ +$config['0370_03ff'][] = ['upper' => 962, 'status' => 'C', 'lower' => [963]]; /* GREEK SMALL LETTER FINAL SIGMA */ +$config['0370_03ff'][] = ['upper' => 976, 'status' => 'C', 'lower' => [946]]; /* GREEK BETA SYMBOL */ +$config['0370_03ff'][] = ['upper' => 977, 'status' => 'C', 'lower' => [952]]; /* GREEK THETA SYMBOL */ +$config['0370_03ff'][] = ['upper' => 981, 'status' => 'C', 'lower' => [966]]; /* GREEK PHI SYMBOL */ +$config['0370_03ff'][] = ['upper' => 982, 'status' => 'C', 'lower' => [960]]; /* GREEK PI SYMBOL */ +$config['0370_03ff'][] = ['upper' => 984, 'status' => 'C', 'lower' => [985]]; /* GREEK LETTER ARCHAIC KOPPA */ +$config['0370_03ff'][] = ['upper' => 986, 'status' => 'C', 'lower' => [987]]; /* GREEK LETTER STIGMA */ +$config['0370_03ff'][] = ['upper' => 988, 'status' => 'C', 'lower' => [989]]; /* GREEK LETTER DIGAMMA */ +$config['0370_03ff'][] = ['upper' => 990, 'status' => 'C', 'lower' => [991]]; /* GREEK LETTER KOPPA */ +$config['0370_03ff'][] = ['upper' => 992, 'status' => 'C', 'lower' => [993]]; /* GREEK LETTER SAMPI */ +$config['0370_03ff'][] = ['upper' => 994, 'status' => 'C', 'lower' => [995]]; /* COPTIC CAPITAL LETTER SHEI */ +$config['0370_03ff'][] = ['upper' => 996, 'status' => 'C', 'lower' => [997]]; /* COPTIC CAPITAL LETTER FEI */ +$config['0370_03ff'][] = ['upper' => 998, 'status' => 'C', 'lower' => [999]]; /* COPTIC CAPITAL LETTER KHEI */ +$config['0370_03ff'][] = ['upper' => 1000, 'status' => 'C', 'lower' => [1001]]; /* COPTIC CAPITAL LETTER HORI */ +$config['0370_03ff'][] = ['upper' => 1002, 'status' => 'C', 'lower' => [1003]]; /* COPTIC CAPITAL LETTER GANGIA */ +$config['0370_03ff'][] = ['upper' => 1004, 'status' => 'C', 'lower' => [1005]]; /* COPTIC CAPITAL LETTER SHIMA */ +$config['0370_03ff'][] = ['upper' => 1006, 'status' => 'C', 'lower' => [1007]]; /* COPTIC CAPITAL LETTER DEI */ +$config['0370_03ff'][] = ['upper' => 1008, 'status' => 'C', 'lower' => [954]]; /* GREEK KAPPA SYMBOL */ +$config['0370_03ff'][] = ['upper' => 1009, 'status' => 'C', 'lower' => [961]]; /* GREEK RHO SYMBOL */ +$config['0370_03ff'][] = ['upper' => 1012, 'status' => 'C', 'lower' => [952]]; /* GREEK CAPITAL THETA SYMBOL */ +$config['0370_03ff'][] = ['upper' => 1013, 'status' => 'C', 'lower' => [949]]; /* GREEK LUNATE EPSILON SYMBOL */ +$config['0370_03ff'][] = ['upper' => 1015, 'status' => 'C', 'lower' => [1016]]; /* GREEK CAPITAL LETTER SHO */ +$config['0370_03ff'][] = ['upper' => 1017, 'status' => 'C', 'lower' => [1010]]; /* GREEK CAPITAL LUNATE SIGMA SYMBOL */ +$config['0370_03ff'][] = ['upper' => 1018, 'status' => 'C', 'lower' => [1019]]; /* GREEK CAPITAL LETTER SAN */ +$config['0370_03ff'][] = ['upper' => 1021, 'status' => 'C', 'lower' => [891]]; /* GREEK CAPITAL REVERSED LUNATE SIGMA SYMBOL */ +$config['0370_03ff'][] = ['upper' => 1022, 'status' => 'C', 'lower' => [892]]; /* GREEK CAPITAL DOTTED LUNATE SIGMA SYMBOL */ +$config['0370_03ff'][] = ['upper' => 1023, 'status' => 'C', 'lower' => [893]]; /* GREEK CAPITAL REVERSED DOTTED LUNATE SIGMA SYMBOL */ diff --git a/lib/Cake/Config/unicode/casefolding/0400_04ff.php b/lib/Cake/Config/unicode/casefolding/0400_04ff.php index fd65be1c..019bb1e8 100755 --- a/lib/Cake/Config/unicode/casefolding/0400_04ff.php +++ b/lib/Cake/Config/unicode/casefolding/0400_04ff.php @@ -27,7 +27,7 @@ * * The lower filed is an array of the decimal values that form the lower case version of a character. * - * The status field is: + * The status field is: * C: common case folding, common mappings shared by both simple and full mappings. * F: full case folding, mappings that cause strings to grow in length. Multiple characters are separated by spaces. * S: simple case folding, mappings to single characters where different from F. @@ -37,127 +37,127 @@ * Note that the Turkic mappings do not maintain canonical equivalence without additional processing. * See the discussions of case mapping in the Unicode Standard for more information. */ -$config['0400_04ff'][] = array('upper' => 1024, 'status' => 'C', 'lower' => array(1104)); /* CYRILLIC CAPITAL LETTER IE WITH GRAVE */ -$config['0400_04ff'][] = array('upper' => 1025, 'status' => 'C', 'lower' => array(1105)); /* CYRILLIC CAPITAL LETTER IO */ -$config['0400_04ff'][] = array('upper' => 1026, 'status' => 'C', 'lower' => array(1106)); /* CYRILLIC CAPITAL LETTER DJE */ -$config['0400_04ff'][] = array('upper' => 1027, 'status' => 'C', 'lower' => array(1107)); /* CYRILLIC CAPITAL LETTER GJE */ -$config['0400_04ff'][] = array('upper' => 1028, 'status' => 'C', 'lower' => array(1108)); /* CYRILLIC CAPITAL LETTER UKRAINIAN IE */ -$config['0400_04ff'][] = array('upper' => 1029, 'status' => 'C', 'lower' => array(1109)); /* CYRILLIC CAPITAL LETTER DZE */ -$config['0400_04ff'][] = array('upper' => 1030, 'status' => 'C', 'lower' => array(1110)); /* CYRILLIC CAPITAL LETTER BYELORUSSIAN-UKRAINIAN I */ -$config['0400_04ff'][] = array('upper' => 1031, 'status' => 'C', 'lower' => array(1111)); /* CYRILLIC CAPITAL LETTER YI */ -$config['0400_04ff'][] = array('upper' => 1032, 'status' => 'C', 'lower' => array(1112)); /* CYRILLIC CAPITAL LETTER JE */ -$config['0400_04ff'][] = array('upper' => 1033, 'status' => 'C', 'lower' => array(1113)); /* CYRILLIC CAPITAL LETTER LJE */ -$config['0400_04ff'][] = array('upper' => 1034, 'status' => 'C', 'lower' => array(1114)); /* CYRILLIC CAPITAL LETTER NJE */ -$config['0400_04ff'][] = array('upper' => 1035, 'status' => 'C', 'lower' => array(1115)); /* CYRILLIC CAPITAL LETTER TSHE */ -$config['0400_04ff'][] = array('upper' => 1036, 'status' => 'C', 'lower' => array(1116)); /* CYRILLIC CAPITAL LETTER KJE */ -$config['0400_04ff'][] = array('upper' => 1037, 'status' => 'C', 'lower' => array(1117)); /* CYRILLIC CAPITAL LETTER I WITH GRAVE */ -$config['0400_04ff'][] = array('upper' => 1038, 'status' => 'C', 'lower' => array(1118)); /* CYRILLIC CAPITAL LETTER SHORT U */ -$config['0400_04ff'][] = array('upper' => 1039, 'status' => 'C', 'lower' => array(1119)); /* CYRILLIC CAPITAL LETTER DZHE */ -$config['0400_04ff'][] = array('upper' => 1040, 'status' => 'C', 'lower' => array(1072)); /* CYRILLIC CAPITAL LETTER A */ -$config['0400_04ff'][] = array('upper' => 1041, 'status' => 'C', 'lower' => array(1073)); /* CYRILLIC CAPITAL LETTER BE */ -$config['0400_04ff'][] = array('upper' => 1042, 'status' => 'C', 'lower' => array(1074)); /* CYRILLIC CAPITAL LETTER VE */ -$config['0400_04ff'][] = array('upper' => 1043, 'status' => 'C', 'lower' => array(1075)); /* CYRILLIC CAPITAL LETTER GHE */ -$config['0400_04ff'][] = array('upper' => 1044, 'status' => 'C', 'lower' => array(1076)); /* CYRILLIC CAPITAL LETTER DE */ -$config['0400_04ff'][] = array('upper' => 1045, 'status' => 'C', 'lower' => array(1077)); /* CYRILLIC CAPITAL LETTER IE */ -$config['0400_04ff'][] = array('upper' => 1046, 'status' => 'C', 'lower' => array(1078)); /* CYRILLIC CAPITAL LETTER ZHE */ -$config['0400_04ff'][] = array('upper' => 1047, 'status' => 'C', 'lower' => array(1079)); /* CYRILLIC CAPITAL LETTER ZE */ -$config['0400_04ff'][] = array('upper' => 1048, 'status' => 'C', 'lower' => array(1080)); /* CYRILLIC CAPITAL LETTER I */ -$config['0400_04ff'][] = array('upper' => 1049, 'status' => 'C', 'lower' => array(1081)); /* CYRILLIC CAPITAL LETTER SHORT I */ -$config['0400_04ff'][] = array('upper' => 1050, 'status' => 'C', 'lower' => array(1082)); /* CYRILLIC CAPITAL LETTER KA */ -$config['0400_04ff'][] = array('upper' => 1051, 'status' => 'C', 'lower' => array(1083)); /* CYRILLIC CAPITAL LETTER EL */ -$config['0400_04ff'][] = array('upper' => 1052, 'status' => 'C', 'lower' => array(1084)); /* CYRILLIC CAPITAL LETTER EM */ -$config['0400_04ff'][] = array('upper' => 1053, 'status' => 'C', 'lower' => array(1085)); /* CYRILLIC CAPITAL LETTER EN */ -$config['0400_04ff'][] = array('upper' => 1054, 'status' => 'C', 'lower' => array(1086)); /* CYRILLIC CAPITAL LETTER O */ -$config['0400_04ff'][] = array('upper' => 1055, 'status' => 'C', 'lower' => array(1087)); /* CYRILLIC CAPITAL LETTER PE */ -$config['0400_04ff'][] = array('upper' => 1056, 'status' => 'C', 'lower' => array(1088)); /* CYRILLIC CAPITAL LETTER ER */ -$config['0400_04ff'][] = array('upper' => 1057, 'status' => 'C', 'lower' => array(1089)); /* CYRILLIC CAPITAL LETTER ES */ -$config['0400_04ff'][] = array('upper' => 1058, 'status' => 'C', 'lower' => array(1090)); /* CYRILLIC CAPITAL LETTER TE */ -$config['0400_04ff'][] = array('upper' => 1059, 'status' => 'C', 'lower' => array(1091)); /* CYRILLIC CAPITAL LETTER U */ -$config['0400_04ff'][] = array('upper' => 1060, 'status' => 'C', 'lower' => array(1092)); /* CYRILLIC CAPITAL LETTER EF */ -$config['0400_04ff'][] = array('upper' => 1061, 'status' => 'C', 'lower' => array(1093)); /* CYRILLIC CAPITAL LETTER HA */ -$config['0400_04ff'][] = array('upper' => 1062, 'status' => 'C', 'lower' => array(1094)); /* CYRILLIC CAPITAL LETTER TSE */ -$config['0400_04ff'][] = array('upper' => 1063, 'status' => 'C', 'lower' => array(1095)); /* CYRILLIC CAPITAL LETTER CHE */ -$config['0400_04ff'][] = array('upper' => 1064, 'status' => 'C', 'lower' => array(1096)); /* CYRILLIC CAPITAL LETTER SHA */ -$config['0400_04ff'][] = array('upper' => 1065, 'status' => 'C', 'lower' => array(1097)); /* CYRILLIC CAPITAL LETTER SHCHA */ -$config['0400_04ff'][] = array('upper' => 1066, 'status' => 'C', 'lower' => array(1098)); /* CYRILLIC CAPITAL LETTER HARD SIGN */ -$config['0400_04ff'][] = array('upper' => 1067, 'status' => 'C', 'lower' => array(1099)); /* CYRILLIC CAPITAL LETTER YERU */ -$config['0400_04ff'][] = array('upper' => 1068, 'status' => 'C', 'lower' => array(1100)); /* CYRILLIC CAPITAL LETTER SOFT SIGN */ -$config['0400_04ff'][] = array('upper' => 1069, 'status' => 'C', 'lower' => array(1101)); /* CYRILLIC CAPITAL LETTER E */ -$config['0400_04ff'][] = array('upper' => 1070, 'status' => 'C', 'lower' => array(1102)); /* CYRILLIC CAPITAL LETTER YU */ -$config['0400_04ff'][] = array('upper' => 1071, 'status' => 'C', 'lower' => array(1103)); /* CYRILLIC CAPITAL LETTER YA */ -$config['0400_04ff'][] = array('upper' => 1120, 'status' => 'C', 'lower' => array(1121)); /* CYRILLIC CAPITAL LETTER OMEGA */ -$config['0400_04ff'][] = array('upper' => 1122, 'status' => 'C', 'lower' => array(1123)); /* CYRILLIC CAPITAL LETTER YAT */ -$config['0400_04ff'][] = array('upper' => 1124, 'status' => 'C', 'lower' => array(1125)); /* CYRILLIC CAPITAL LETTER IOTIFIED E */ -$config['0400_04ff'][] = array('upper' => 1126, 'status' => 'C', 'lower' => array(1127)); /* CYRILLIC CAPITAL LETTER LITTLE YUS */ -$config['0400_04ff'][] = array('upper' => 1128, 'status' => 'C', 'lower' => array(1129)); /* CYRILLIC CAPITAL LETTER IOTIFIED LITTLE YUS */ -$config['0400_04ff'][] = array('upper' => 1130, 'status' => 'C', 'lower' => array(1131)); /* CYRILLIC CAPITAL LETTER BIG YUS */ -$config['0400_04ff'][] = array('upper' => 1132, 'status' => 'C', 'lower' => array(1133)); /* CYRILLIC CAPITAL LETTER IOTIFIED BIG YUS */ -$config['0400_04ff'][] = array('upper' => 1134, 'status' => 'C', 'lower' => array(1135)); /* CYRILLIC CAPITAL LETTER KSI */ -$config['0400_04ff'][] = array('upper' => 1136, 'status' => 'C', 'lower' => array(1137)); /* CYRILLIC CAPITAL LETTER PSI */ -$config['0400_04ff'][] = array('upper' => 1138, 'status' => 'C', 'lower' => array(1139)); /* CYRILLIC CAPITAL LETTER FITA */ -$config['0400_04ff'][] = array('upper' => 1140, 'status' => 'C', 'lower' => array(1141)); /* CYRILLIC CAPITAL LETTER IZHITSA */ -$config['0400_04ff'][] = array('upper' => 1142, 'status' => 'C', 'lower' => array(1143)); /* CYRILLIC CAPITAL LETTER IZHITSA WITH DOUBLE GRAVE ACCENT */ -$config['0400_04ff'][] = array('upper' => 1144, 'status' => 'C', 'lower' => array(1145)); /* CYRILLIC CAPITAL LETTER UK */ -$config['0400_04ff'][] = array('upper' => 1146, 'status' => 'C', 'lower' => array(1147)); /* CYRILLIC CAPITAL LETTER ROUND OMEGA */ -$config['0400_04ff'][] = array('upper' => 1148, 'status' => 'C', 'lower' => array(1149)); /* CYRILLIC CAPITAL LETTER OMEGA WITH TITLO */ -$config['0400_04ff'][] = array('upper' => 1150, 'status' => 'C', 'lower' => array(1151)); /* CYRILLIC CAPITAL LETTER OT */ -$config['0400_04ff'][] = array('upper' => 1152, 'status' => 'C', 'lower' => array(1153)); /* CYRILLIC CAPITAL LETTER KOPPA */ -$config['0400_04ff'][] = array('upper' => 1162, 'status' => 'C', 'lower' => array(1163)); /* CYRILLIC CAPITAL LETTER SHORT I WITH TAIL */ -$config['0400_04ff'][] = array('upper' => 1164, 'status' => 'C', 'lower' => array(1165)); /* CYRILLIC CAPITAL LETTER SEMISOFT SIGN */ -$config['0400_04ff'][] = array('upper' => 1166, 'status' => 'C', 'lower' => array(1167)); /* CYRILLIC CAPITAL LETTER ER WITH TICK */ -$config['0400_04ff'][] = array('upper' => 1168, 'status' => 'C', 'lower' => array(1169)); /* CYRILLIC CAPITAL LETTER GHE WITH UPTURN */ -$config['0400_04ff'][] = array('upper' => 1170, 'status' => 'C', 'lower' => array(1171)); /* CYRILLIC CAPITAL LETTER GHE WITH STROKE */ -$config['0400_04ff'][] = array('upper' => 1172, 'status' => 'C', 'lower' => array(1173)); /* CYRILLIC CAPITAL LETTER GHE WITH MIDDLE HOOK */ -$config['0400_04ff'][] = array('upper' => 1174, 'status' => 'C', 'lower' => array(1175)); /* CYRILLIC CAPITAL LETTER ZHE WITH DESCENDER */ -$config['0400_04ff'][] = array('upper' => 1176, 'status' => 'C', 'lower' => array(1177)); /* CYRILLIC CAPITAL LETTER ZE WITH DESCENDER */ -$config['0400_04ff'][] = array('upper' => 1178, 'status' => 'C', 'lower' => array(1179)); /* CYRILLIC CAPITAL LETTER KA WITH DESCENDER */ -$config['0400_04ff'][] = array('upper' => 1180, 'status' => 'C', 'lower' => array(1181)); /* CYRILLIC CAPITAL LETTER KA WITH VERTICAL STROKE */ -$config['0400_04ff'][] = array('upper' => 1182, 'status' => 'C', 'lower' => array(1183)); /* CYRILLIC CAPITAL LETTER KA WITH STROKE */ -$config['0400_04ff'][] = array('upper' => 1184, 'status' => 'C', 'lower' => array(1185)); /* CYRILLIC CAPITAL LETTER BASHKIR KA */ -$config['0400_04ff'][] = array('upper' => 1186, 'status' => 'C', 'lower' => array(1187)); /* CYRILLIC CAPITAL LETTER EN WITH DESCENDER */ -$config['0400_04ff'][] = array('upper' => 1188, 'status' => 'C', 'lower' => array(1189)); /* CYRILLIC CAPITAL LIGATURE EN GHE */ -$config['0400_04ff'][] = array('upper' => 1190, 'status' => 'C', 'lower' => array(1191)); /* CYRILLIC CAPITAL LETTER PE WITH MIDDLE HOOK */ -$config['0400_04ff'][] = array('upper' => 1192, 'status' => 'C', 'lower' => array(1193)); /* CYRILLIC CAPITAL LETTER ABKHASIAN HA */ -$config['0400_04ff'][] = array('upper' => 1194, 'status' => 'C', 'lower' => array(1195)); /* CYRILLIC CAPITAL LETTER ES WITH DESCENDER */ -$config['0400_04ff'][] = array('upper' => 1196, 'status' => 'C', 'lower' => array(1197)); /* CYRILLIC CAPITAL LETTER TE WITH DESCENDER */ -$config['0400_04ff'][] = array('upper' => 1198, 'status' => 'C', 'lower' => array(1199)); /* CYRILLIC CAPITAL LETTER STRAIGHT U */ -$config['0400_04ff'][] = array('upper' => 1200, 'status' => 'C', 'lower' => array(1201)); /* CYRILLIC CAPITAL LETTER STRAIGHT U WITH STROKE */ -$config['0400_04ff'][] = array('upper' => 1202, 'status' => 'C', 'lower' => array(1203)); /* CYRILLIC CAPITAL LETTER HA WITH DESCENDER */ -$config['0400_04ff'][] = array('upper' => 1204, 'status' => 'C', 'lower' => array(1205)); /* CYRILLIC CAPITAL LIGATURE TE TSE */ -$config['0400_04ff'][] = array('upper' => 1206, 'status' => 'C', 'lower' => array(1207)); /* CYRILLIC CAPITAL LETTER CHE WITH DESCENDER */ -$config['0400_04ff'][] = array('upper' => 1208, 'status' => 'C', 'lower' => array(1209)); /* CYRILLIC CAPITAL LETTER CHE WITH VERTICAL STROKE */ -$config['0400_04ff'][] = array('upper' => 1210, 'status' => 'C', 'lower' => array(1211)); /* CYRILLIC CAPITAL LETTER SHHA */ -$config['0400_04ff'][] = array('upper' => 1212, 'status' => 'C', 'lower' => array(1213)); /* CYRILLIC CAPITAL LETTER ABKHASIAN CHE */ -$config['0400_04ff'][] = array('upper' => 1214, 'status' => 'C', 'lower' => array(1215)); /* CYRILLIC CAPITAL LETTER ABKHASIAN CHE WITH DESCENDER */ -$config['0400_04ff'][] = array('upper' => 1216, 'status' => 'C', 'lower' => array(1231)); /* CYRILLIC LETTER PALOCHKA */ -$config['0400_04ff'][] = array('upper' => 1217, 'status' => 'C', 'lower' => array(1218)); /* CYRILLIC CAPITAL LETTER ZHE WITH BREVE */ -$config['0400_04ff'][] = array('upper' => 1219, 'status' => 'C', 'lower' => array(1220)); /* CYRILLIC CAPITAL LETTER KA WITH HOOK */ -$config['0400_04ff'][] = array('upper' => 1221, 'status' => 'C', 'lower' => array(1222)); /* CYRILLIC CAPITAL LETTER EL WITH TAIL */ -$config['0400_04ff'][] = array('upper' => 1223, 'status' => 'C', 'lower' => array(1224)); /* CYRILLIC CAPITAL LETTER EN WITH HOOK */ -$config['0400_04ff'][] = array('upper' => 1225, 'status' => 'C', 'lower' => array(1226)); /* CYRILLIC CAPITAL LETTER EN WITH TAIL */ -$config['0400_04ff'][] = array('upper' => 1227, 'status' => 'C', 'lower' => array(1228)); /* CYRILLIC CAPITAL LETTER KHAKASSIAN CHE */ -$config['0400_04ff'][] = array('upper' => 1229, 'status' => 'C', 'lower' => array(1230)); /* CYRILLIC CAPITAL LETTER EM WITH TAIL */ -$config['0400_04ff'][] = array('upper' => 1232, 'status' => 'C', 'lower' => array(1233)); /* CYRILLIC CAPITAL LETTER A WITH BREVE */ -$config['0400_04ff'][] = array('upper' => 1234, 'status' => 'C', 'lower' => array(1235)); /* CYRILLIC CAPITAL LETTER A WITH DIAERESIS */ -$config['0400_04ff'][] = array('upper' => 1236, 'status' => 'C', 'lower' => array(1237)); /* CYRILLIC CAPITAL LIGATURE A IE */ -$config['0400_04ff'][] = array('upper' => 1238, 'status' => 'C', 'lower' => array(1239)); /* CYRILLIC CAPITAL LETTER IE WITH BREVE */ -$config['0400_04ff'][] = array('upper' => 1240, 'status' => 'C', 'lower' => array(1241)); /* CYRILLIC CAPITAL LETTER SCHWA */ -$config['0400_04ff'][] = array('upper' => 1242, 'status' => 'C', 'lower' => array(1243)); /* CYRILLIC CAPITAL LETTER SCHWA WITH DIAERESIS */ -$config['0400_04ff'][] = array('upper' => 1244, 'status' => 'C', 'lower' => array(1245)); /* CYRILLIC CAPITAL LETTER ZHE WITH DIAERESIS */ -$config['0400_04ff'][] = array('upper' => 1246, 'status' => 'C', 'lower' => array(1247)); /* CYRILLIC CAPITAL LETTER ZE WITH DIAERESIS */ -$config['0400_04ff'][] = array('upper' => 1248, 'status' => 'C', 'lower' => array(1249)); /* CYRILLIC CAPITAL LETTER ABKHASIAN DZE */ -$config['0400_04ff'][] = array('upper' => 1250, 'status' => 'C', 'lower' => array(1251)); /* CYRILLIC CAPITAL LETTER I WITH MACRON */ -$config['0400_04ff'][] = array('upper' => 1252, 'status' => 'C', 'lower' => array(1253)); /* CYRILLIC CAPITAL LETTER I WITH DIAERESIS */ -$config['0400_04ff'][] = array('upper' => 1254, 'status' => 'C', 'lower' => array(1255)); /* CYRILLIC CAPITAL LETTER O WITH DIAERESIS */ -$config['0400_04ff'][] = array('upper' => 1256, 'status' => 'C', 'lower' => array(1257)); /* CYRILLIC CAPITAL LETTER BARRED O */ -$config['0400_04ff'][] = array('upper' => 1258, 'status' => 'C', 'lower' => array(1259)); /* CYRILLIC CAPITAL LETTER BARRED O WITH DIAERESIS */ -$config['0400_04ff'][] = array('upper' => 1260, 'status' => 'C', 'lower' => array(1261)); /* CYRILLIC CAPITAL LETTER E WITH DIAERESIS */ -$config['0400_04ff'][] = array('upper' => 1262, 'status' => 'C', 'lower' => array(1263)); /* CYRILLIC CAPITAL LETTER U WITH MACRON */ -$config['0400_04ff'][] = array('upper' => 1264, 'status' => 'C', 'lower' => array(1265)); /* CYRILLIC CAPITAL LETTER U WITH DIAERESIS */ -$config['0400_04ff'][] = array('upper' => 1266, 'status' => 'C', 'lower' => array(1267)); /* CYRILLIC CAPITAL LETTER U WITH DOUBLE ACUTE */ -$config['0400_04ff'][] = array('upper' => 1268, 'status' => 'C', 'lower' => array(1269)); /* CYRILLIC CAPITAL LETTER CHE WITH DIAERESIS */ -$config['0400_04ff'][] = array('upper' => 1270, 'status' => 'C', 'lower' => array(1271)); /* CYRILLIC CAPITAL LETTER GHE WITH DESCENDER */ -$config['0400_04ff'][] = array('upper' => 1272, 'status' => 'C', 'lower' => array(1273)); /* CYRILLIC CAPITAL LETTER YERU WITH DIAERESIS */ -$config['0400_04ff'][] = array('upper' => 1274, 'status' => 'C', 'lower' => array(1275)); /* CYRILLIC CAPITAL LETTER GHE WITH STROKE AND HOOK */ -$config['0400_04ff'][] = array('upper' => 1276, 'status' => 'C', 'lower' => array(1277)); /* CYRILLIC CAPITAL LETTER HA WITH HOOK */ -$config['0400_04ff'][] = array('upper' => 1278, 'status' => 'C', 'lower' => array(1279)); /* CYRILLIC CAPITAL LETTER HA WITH STROKE */ +$config['0400_04ff'][] = ['upper' => 1024, 'status' => 'C', 'lower' => [1104]]; /* CYRILLIC CAPITAL LETTER IE WITH GRAVE */ +$config['0400_04ff'][] = ['upper' => 1025, 'status' => 'C', 'lower' => [1105]]; /* CYRILLIC CAPITAL LETTER IO */ +$config['0400_04ff'][] = ['upper' => 1026, 'status' => 'C', 'lower' => [1106]]; /* CYRILLIC CAPITAL LETTER DJE */ +$config['0400_04ff'][] = ['upper' => 1027, 'status' => 'C', 'lower' => [1107]]; /* CYRILLIC CAPITAL LETTER GJE */ +$config['0400_04ff'][] = ['upper' => 1028, 'status' => 'C', 'lower' => [1108]]; /* CYRILLIC CAPITAL LETTER UKRAINIAN IE */ +$config['0400_04ff'][] = ['upper' => 1029, 'status' => 'C', 'lower' => [1109]]; /* CYRILLIC CAPITAL LETTER DZE */ +$config['0400_04ff'][] = ['upper' => 1030, 'status' => 'C', 'lower' => [1110]]; /* CYRILLIC CAPITAL LETTER BYELORUSSIAN-UKRAINIAN I */ +$config['0400_04ff'][] = ['upper' => 1031, 'status' => 'C', 'lower' => [1111]]; /* CYRILLIC CAPITAL LETTER YI */ +$config['0400_04ff'][] = ['upper' => 1032, 'status' => 'C', 'lower' => [1112]]; /* CYRILLIC CAPITAL LETTER JE */ +$config['0400_04ff'][] = ['upper' => 1033, 'status' => 'C', 'lower' => [1113]]; /* CYRILLIC CAPITAL LETTER LJE */ +$config['0400_04ff'][] = ['upper' => 1034, 'status' => 'C', 'lower' => [1114]]; /* CYRILLIC CAPITAL LETTER NJE */ +$config['0400_04ff'][] = ['upper' => 1035, 'status' => 'C', 'lower' => [1115]]; /* CYRILLIC CAPITAL LETTER TSHE */ +$config['0400_04ff'][] = ['upper' => 1036, 'status' => 'C', 'lower' => [1116]]; /* CYRILLIC CAPITAL LETTER KJE */ +$config['0400_04ff'][] = ['upper' => 1037, 'status' => 'C', 'lower' => [1117]]; /* CYRILLIC CAPITAL LETTER I WITH GRAVE */ +$config['0400_04ff'][] = ['upper' => 1038, 'status' => 'C', 'lower' => [1118]]; /* CYRILLIC CAPITAL LETTER SHORT U */ +$config['0400_04ff'][] = ['upper' => 1039, 'status' => 'C', 'lower' => [1119]]; /* CYRILLIC CAPITAL LETTER DZHE */ +$config['0400_04ff'][] = ['upper' => 1040, 'status' => 'C', 'lower' => [1072]]; /* CYRILLIC CAPITAL LETTER A */ +$config['0400_04ff'][] = ['upper' => 1041, 'status' => 'C', 'lower' => [1073]]; /* CYRILLIC CAPITAL LETTER BE */ +$config['0400_04ff'][] = ['upper' => 1042, 'status' => 'C', 'lower' => [1074]]; /* CYRILLIC CAPITAL LETTER VE */ +$config['0400_04ff'][] = ['upper' => 1043, 'status' => 'C', 'lower' => [1075]]; /* CYRILLIC CAPITAL LETTER GHE */ +$config['0400_04ff'][] = ['upper' => 1044, 'status' => 'C', 'lower' => [1076]]; /* CYRILLIC CAPITAL LETTER DE */ +$config['0400_04ff'][] = ['upper' => 1045, 'status' => 'C', 'lower' => [1077]]; /* CYRILLIC CAPITAL LETTER IE */ +$config['0400_04ff'][] = ['upper' => 1046, 'status' => 'C', 'lower' => [1078]]; /* CYRILLIC CAPITAL LETTER ZHE */ +$config['0400_04ff'][] = ['upper' => 1047, 'status' => 'C', 'lower' => [1079]]; /* CYRILLIC CAPITAL LETTER ZE */ +$config['0400_04ff'][] = ['upper' => 1048, 'status' => 'C', 'lower' => [1080]]; /* CYRILLIC CAPITAL LETTER I */ +$config['0400_04ff'][] = ['upper' => 1049, 'status' => 'C', 'lower' => [1081]]; /* CYRILLIC CAPITAL LETTER SHORT I */ +$config['0400_04ff'][] = ['upper' => 1050, 'status' => 'C', 'lower' => [1082]]; /* CYRILLIC CAPITAL LETTER KA */ +$config['0400_04ff'][] = ['upper' => 1051, 'status' => 'C', 'lower' => [1083]]; /* CYRILLIC CAPITAL LETTER EL */ +$config['0400_04ff'][] = ['upper' => 1052, 'status' => 'C', 'lower' => [1084]]; /* CYRILLIC CAPITAL LETTER EM */ +$config['0400_04ff'][] = ['upper' => 1053, 'status' => 'C', 'lower' => [1085]]; /* CYRILLIC CAPITAL LETTER EN */ +$config['0400_04ff'][] = ['upper' => 1054, 'status' => 'C', 'lower' => [1086]]; /* CYRILLIC CAPITAL LETTER O */ +$config['0400_04ff'][] = ['upper' => 1055, 'status' => 'C', 'lower' => [1087]]; /* CYRILLIC CAPITAL LETTER PE */ +$config['0400_04ff'][] = ['upper' => 1056, 'status' => 'C', 'lower' => [1088]]; /* CYRILLIC CAPITAL LETTER ER */ +$config['0400_04ff'][] = ['upper' => 1057, 'status' => 'C', 'lower' => [1089]]; /* CYRILLIC CAPITAL LETTER ES */ +$config['0400_04ff'][] = ['upper' => 1058, 'status' => 'C', 'lower' => [1090]]; /* CYRILLIC CAPITAL LETTER TE */ +$config['0400_04ff'][] = ['upper' => 1059, 'status' => 'C', 'lower' => [1091]]; /* CYRILLIC CAPITAL LETTER U */ +$config['0400_04ff'][] = ['upper' => 1060, 'status' => 'C', 'lower' => [1092]]; /* CYRILLIC CAPITAL LETTER EF */ +$config['0400_04ff'][] = ['upper' => 1061, 'status' => 'C', 'lower' => [1093]]; /* CYRILLIC CAPITAL LETTER HA */ +$config['0400_04ff'][] = ['upper' => 1062, 'status' => 'C', 'lower' => [1094]]; /* CYRILLIC CAPITAL LETTER TSE */ +$config['0400_04ff'][] = ['upper' => 1063, 'status' => 'C', 'lower' => [1095]]; /* CYRILLIC CAPITAL LETTER CHE */ +$config['0400_04ff'][] = ['upper' => 1064, 'status' => 'C', 'lower' => [1096]]; /* CYRILLIC CAPITAL LETTER SHA */ +$config['0400_04ff'][] = ['upper' => 1065, 'status' => 'C', 'lower' => [1097]]; /* CYRILLIC CAPITAL LETTER SHCHA */ +$config['0400_04ff'][] = ['upper' => 1066, 'status' => 'C', 'lower' => [1098]]; /* CYRILLIC CAPITAL LETTER HARD SIGN */ +$config['0400_04ff'][] = ['upper' => 1067, 'status' => 'C', 'lower' => [1099]]; /* CYRILLIC CAPITAL LETTER YERU */ +$config['0400_04ff'][] = ['upper' => 1068, 'status' => 'C', 'lower' => [1100]]; /* CYRILLIC CAPITAL LETTER SOFT SIGN */ +$config['0400_04ff'][] = ['upper' => 1069, 'status' => 'C', 'lower' => [1101]]; /* CYRILLIC CAPITAL LETTER E */ +$config['0400_04ff'][] = ['upper' => 1070, 'status' => 'C', 'lower' => [1102]]; /* CYRILLIC CAPITAL LETTER YU */ +$config['0400_04ff'][] = ['upper' => 1071, 'status' => 'C', 'lower' => [1103]]; /* CYRILLIC CAPITAL LETTER YA */ +$config['0400_04ff'][] = ['upper' => 1120, 'status' => 'C', 'lower' => [1121]]; /* CYRILLIC CAPITAL LETTER OMEGA */ +$config['0400_04ff'][] = ['upper' => 1122, 'status' => 'C', 'lower' => [1123]]; /* CYRILLIC CAPITAL LETTER YAT */ +$config['0400_04ff'][] = ['upper' => 1124, 'status' => 'C', 'lower' => [1125]]; /* CYRILLIC CAPITAL LETTER IOTIFIED E */ +$config['0400_04ff'][] = ['upper' => 1126, 'status' => 'C', 'lower' => [1127]]; /* CYRILLIC CAPITAL LETTER LITTLE YUS */ +$config['0400_04ff'][] = ['upper' => 1128, 'status' => 'C', 'lower' => [1129]]; /* CYRILLIC CAPITAL LETTER IOTIFIED LITTLE YUS */ +$config['0400_04ff'][] = ['upper' => 1130, 'status' => 'C', 'lower' => [1131]]; /* CYRILLIC CAPITAL LETTER BIG YUS */ +$config['0400_04ff'][] = ['upper' => 1132, 'status' => 'C', 'lower' => [1133]]; /* CYRILLIC CAPITAL LETTER IOTIFIED BIG YUS */ +$config['0400_04ff'][] = ['upper' => 1134, 'status' => 'C', 'lower' => [1135]]; /* CYRILLIC CAPITAL LETTER KSI */ +$config['0400_04ff'][] = ['upper' => 1136, 'status' => 'C', 'lower' => [1137]]; /* CYRILLIC CAPITAL LETTER PSI */ +$config['0400_04ff'][] = ['upper' => 1138, 'status' => 'C', 'lower' => [1139]]; /* CYRILLIC CAPITAL LETTER FITA */ +$config['0400_04ff'][] = ['upper' => 1140, 'status' => 'C', 'lower' => [1141]]; /* CYRILLIC CAPITAL LETTER IZHITSA */ +$config['0400_04ff'][] = ['upper' => 1142, 'status' => 'C', 'lower' => [1143]]; /* CYRILLIC CAPITAL LETTER IZHITSA WITH DOUBLE GRAVE ACCENT */ +$config['0400_04ff'][] = ['upper' => 1144, 'status' => 'C', 'lower' => [1145]]; /* CYRILLIC CAPITAL LETTER UK */ +$config['0400_04ff'][] = ['upper' => 1146, 'status' => 'C', 'lower' => [1147]]; /* CYRILLIC CAPITAL LETTER ROUND OMEGA */ +$config['0400_04ff'][] = ['upper' => 1148, 'status' => 'C', 'lower' => [1149]]; /* CYRILLIC CAPITAL LETTER OMEGA WITH TITLO */ +$config['0400_04ff'][] = ['upper' => 1150, 'status' => 'C', 'lower' => [1151]]; /* CYRILLIC CAPITAL LETTER OT */ +$config['0400_04ff'][] = ['upper' => 1152, 'status' => 'C', 'lower' => [1153]]; /* CYRILLIC CAPITAL LETTER KOPPA */ +$config['0400_04ff'][] = ['upper' => 1162, 'status' => 'C', 'lower' => [1163]]; /* CYRILLIC CAPITAL LETTER SHORT I WITH TAIL */ +$config['0400_04ff'][] = ['upper' => 1164, 'status' => 'C', 'lower' => [1165]]; /* CYRILLIC CAPITAL LETTER SEMISOFT SIGN */ +$config['0400_04ff'][] = ['upper' => 1166, 'status' => 'C', 'lower' => [1167]]; /* CYRILLIC CAPITAL LETTER ER WITH TICK */ +$config['0400_04ff'][] = ['upper' => 1168, 'status' => 'C', 'lower' => [1169]]; /* CYRILLIC CAPITAL LETTER GHE WITH UPTURN */ +$config['0400_04ff'][] = ['upper' => 1170, 'status' => 'C', 'lower' => [1171]]; /* CYRILLIC CAPITAL LETTER GHE WITH STROKE */ +$config['0400_04ff'][] = ['upper' => 1172, 'status' => 'C', 'lower' => [1173]]; /* CYRILLIC CAPITAL LETTER GHE WITH MIDDLE HOOK */ +$config['0400_04ff'][] = ['upper' => 1174, 'status' => 'C', 'lower' => [1175]]; /* CYRILLIC CAPITAL LETTER ZHE WITH DESCENDER */ +$config['0400_04ff'][] = ['upper' => 1176, 'status' => 'C', 'lower' => [1177]]; /* CYRILLIC CAPITAL LETTER ZE WITH DESCENDER */ +$config['0400_04ff'][] = ['upper' => 1178, 'status' => 'C', 'lower' => [1179]]; /* CYRILLIC CAPITAL LETTER KA WITH DESCENDER */ +$config['0400_04ff'][] = ['upper' => 1180, 'status' => 'C', 'lower' => [1181]]; /* CYRILLIC CAPITAL LETTER KA WITH VERTICAL STROKE */ +$config['0400_04ff'][] = ['upper' => 1182, 'status' => 'C', 'lower' => [1183]]; /* CYRILLIC CAPITAL LETTER KA WITH STROKE */ +$config['0400_04ff'][] = ['upper' => 1184, 'status' => 'C', 'lower' => [1185]]; /* CYRILLIC CAPITAL LETTER BASHKIR KA */ +$config['0400_04ff'][] = ['upper' => 1186, 'status' => 'C', 'lower' => [1187]]; /* CYRILLIC CAPITAL LETTER EN WITH DESCENDER */ +$config['0400_04ff'][] = ['upper' => 1188, 'status' => 'C', 'lower' => [1189]]; /* CYRILLIC CAPITAL LIGATURE EN GHE */ +$config['0400_04ff'][] = ['upper' => 1190, 'status' => 'C', 'lower' => [1191]]; /* CYRILLIC CAPITAL LETTER PE WITH MIDDLE HOOK */ +$config['0400_04ff'][] = ['upper' => 1192, 'status' => 'C', 'lower' => [1193]]; /* CYRILLIC CAPITAL LETTER ABKHASIAN HA */ +$config['0400_04ff'][] = ['upper' => 1194, 'status' => 'C', 'lower' => [1195]]; /* CYRILLIC CAPITAL LETTER ES WITH DESCENDER */ +$config['0400_04ff'][] = ['upper' => 1196, 'status' => 'C', 'lower' => [1197]]; /* CYRILLIC CAPITAL LETTER TE WITH DESCENDER */ +$config['0400_04ff'][] = ['upper' => 1198, 'status' => 'C', 'lower' => [1199]]; /* CYRILLIC CAPITAL LETTER STRAIGHT U */ +$config['0400_04ff'][] = ['upper' => 1200, 'status' => 'C', 'lower' => [1201]]; /* CYRILLIC CAPITAL LETTER STRAIGHT U WITH STROKE */ +$config['0400_04ff'][] = ['upper' => 1202, 'status' => 'C', 'lower' => [1203]]; /* CYRILLIC CAPITAL LETTER HA WITH DESCENDER */ +$config['0400_04ff'][] = ['upper' => 1204, 'status' => 'C', 'lower' => [1205]]; /* CYRILLIC CAPITAL LIGATURE TE TSE */ +$config['0400_04ff'][] = ['upper' => 1206, 'status' => 'C', 'lower' => [1207]]; /* CYRILLIC CAPITAL LETTER CHE WITH DESCENDER */ +$config['0400_04ff'][] = ['upper' => 1208, 'status' => 'C', 'lower' => [1209]]; /* CYRILLIC CAPITAL LETTER CHE WITH VERTICAL STROKE */ +$config['0400_04ff'][] = ['upper' => 1210, 'status' => 'C', 'lower' => [1211]]; /* CYRILLIC CAPITAL LETTER SHHA */ +$config['0400_04ff'][] = ['upper' => 1212, 'status' => 'C', 'lower' => [1213]]; /* CYRILLIC CAPITAL LETTER ABKHASIAN CHE */ +$config['0400_04ff'][] = ['upper' => 1214, 'status' => 'C', 'lower' => [1215]]; /* CYRILLIC CAPITAL LETTER ABKHASIAN CHE WITH DESCENDER */ +$config['0400_04ff'][] = ['upper' => 1216, 'status' => 'C', 'lower' => [1231]]; /* CYRILLIC LETTER PALOCHKA */ +$config['0400_04ff'][] = ['upper' => 1217, 'status' => 'C', 'lower' => [1218]]; /* CYRILLIC CAPITAL LETTER ZHE WITH BREVE */ +$config['0400_04ff'][] = ['upper' => 1219, 'status' => 'C', 'lower' => [1220]]; /* CYRILLIC CAPITAL LETTER KA WITH HOOK */ +$config['0400_04ff'][] = ['upper' => 1221, 'status' => 'C', 'lower' => [1222]]; /* CYRILLIC CAPITAL LETTER EL WITH TAIL */ +$config['0400_04ff'][] = ['upper' => 1223, 'status' => 'C', 'lower' => [1224]]; /* CYRILLIC CAPITAL LETTER EN WITH HOOK */ +$config['0400_04ff'][] = ['upper' => 1225, 'status' => 'C', 'lower' => [1226]]; /* CYRILLIC CAPITAL LETTER EN WITH TAIL */ +$config['0400_04ff'][] = ['upper' => 1227, 'status' => 'C', 'lower' => [1228]]; /* CYRILLIC CAPITAL LETTER KHAKASSIAN CHE */ +$config['0400_04ff'][] = ['upper' => 1229, 'status' => 'C', 'lower' => [1230]]; /* CYRILLIC CAPITAL LETTER EM WITH TAIL */ +$config['0400_04ff'][] = ['upper' => 1232, 'status' => 'C', 'lower' => [1233]]; /* CYRILLIC CAPITAL LETTER A WITH BREVE */ +$config['0400_04ff'][] = ['upper' => 1234, 'status' => 'C', 'lower' => [1235]]; /* CYRILLIC CAPITAL LETTER A WITH DIAERESIS */ +$config['0400_04ff'][] = ['upper' => 1236, 'status' => 'C', 'lower' => [1237]]; /* CYRILLIC CAPITAL LIGATURE A IE */ +$config['0400_04ff'][] = ['upper' => 1238, 'status' => 'C', 'lower' => [1239]]; /* CYRILLIC CAPITAL LETTER IE WITH BREVE */ +$config['0400_04ff'][] = ['upper' => 1240, 'status' => 'C', 'lower' => [1241]]; /* CYRILLIC CAPITAL LETTER SCHWA */ +$config['0400_04ff'][] = ['upper' => 1242, 'status' => 'C', 'lower' => [1243]]; /* CYRILLIC CAPITAL LETTER SCHWA WITH DIAERESIS */ +$config['0400_04ff'][] = ['upper' => 1244, 'status' => 'C', 'lower' => [1245]]; /* CYRILLIC CAPITAL LETTER ZHE WITH DIAERESIS */ +$config['0400_04ff'][] = ['upper' => 1246, 'status' => 'C', 'lower' => [1247]]; /* CYRILLIC CAPITAL LETTER ZE WITH DIAERESIS */ +$config['0400_04ff'][] = ['upper' => 1248, 'status' => 'C', 'lower' => [1249]]; /* CYRILLIC CAPITAL LETTER ABKHASIAN DZE */ +$config['0400_04ff'][] = ['upper' => 1250, 'status' => 'C', 'lower' => [1251]]; /* CYRILLIC CAPITAL LETTER I WITH MACRON */ +$config['0400_04ff'][] = ['upper' => 1252, 'status' => 'C', 'lower' => [1253]]; /* CYRILLIC CAPITAL LETTER I WITH DIAERESIS */ +$config['0400_04ff'][] = ['upper' => 1254, 'status' => 'C', 'lower' => [1255]]; /* CYRILLIC CAPITAL LETTER O WITH DIAERESIS */ +$config['0400_04ff'][] = ['upper' => 1256, 'status' => 'C', 'lower' => [1257]]; /* CYRILLIC CAPITAL LETTER BARRED O */ +$config['0400_04ff'][] = ['upper' => 1258, 'status' => 'C', 'lower' => [1259]]; /* CYRILLIC CAPITAL LETTER BARRED O WITH DIAERESIS */ +$config['0400_04ff'][] = ['upper' => 1260, 'status' => 'C', 'lower' => [1261]]; /* CYRILLIC CAPITAL LETTER E WITH DIAERESIS */ +$config['0400_04ff'][] = ['upper' => 1262, 'status' => 'C', 'lower' => [1263]]; /* CYRILLIC CAPITAL LETTER U WITH MACRON */ +$config['0400_04ff'][] = ['upper' => 1264, 'status' => 'C', 'lower' => [1265]]; /* CYRILLIC CAPITAL LETTER U WITH DIAERESIS */ +$config['0400_04ff'][] = ['upper' => 1266, 'status' => 'C', 'lower' => [1267]]; /* CYRILLIC CAPITAL LETTER U WITH DOUBLE ACUTE */ +$config['0400_04ff'][] = ['upper' => 1268, 'status' => 'C', 'lower' => [1269]]; /* CYRILLIC CAPITAL LETTER CHE WITH DIAERESIS */ +$config['0400_04ff'][] = ['upper' => 1270, 'status' => 'C', 'lower' => [1271]]; /* CYRILLIC CAPITAL LETTER GHE WITH DESCENDER */ +$config['0400_04ff'][] = ['upper' => 1272, 'status' => 'C', 'lower' => [1273]]; /* CYRILLIC CAPITAL LETTER YERU WITH DIAERESIS */ +$config['0400_04ff'][] = ['upper' => 1274, 'status' => 'C', 'lower' => [1275]]; /* CYRILLIC CAPITAL LETTER GHE WITH STROKE AND HOOK */ +$config['0400_04ff'][] = ['upper' => 1276, 'status' => 'C', 'lower' => [1277]]; /* CYRILLIC CAPITAL LETTER HA WITH HOOK */ +$config['0400_04ff'][] = ['upper' => 1278, 'status' => 'C', 'lower' => [1279]]; /* CYRILLIC CAPITAL LETTER HA WITH STROKE */ diff --git a/lib/Cake/Config/unicode/casefolding/0500_052f.php b/lib/Cake/Config/unicode/casefolding/0500_052f.php index c5f4373f..3e6a7283 100755 --- a/lib/Cake/Config/unicode/casefolding/0500_052f.php +++ b/lib/Cake/Config/unicode/casefolding/0500_052f.php @@ -27,7 +27,7 @@ * * The lower filed is an array of the decimal values that form the lower case version of a character. * - * The status field is: + * The status field is: * C: common case folding, common mappings shared by both simple and full mappings. * F: full case folding, mappings that cause strings to grow in length. Multiple characters are separated by spaces. * S: simple case folding, mappings to single characters where different from F. @@ -37,13 +37,13 @@ * Note that the Turkic mappings do not maintain canonical equivalence without additional processing. * See the discussions of case mapping in the Unicode Standard for more information. */ -$config['0500_052f'][] = array('upper' => 1280, 'status' => 'C', 'lower' => array(1281)); /* CYRILLIC CAPITAL LETTER KOMI DE */ -$config['0500_052f'][] = array('upper' => 1282, 'status' => 'C', 'lower' => array(1283)); /* CYRILLIC CAPITAL LETTER KOMI DJE */ -$config['0500_052f'][] = array('upper' => 1284, 'status' => 'C', 'lower' => array(1285)); /* CYRILLIC CAPITAL LETTER KOMI ZJE */ -$config['0500_052f'][] = array('upper' => 1286, 'status' => 'C', 'lower' => array(1287)); /* CYRILLIC CAPITAL LETTER KOMI DZJE */ -$config['0500_052f'][] = array('upper' => 1288, 'status' => 'C', 'lower' => array(1289)); /* CYRILLIC CAPITAL LETTER KOMI LJE */ -$config['0500_052f'][] = array('upper' => 1290, 'status' => 'C', 'lower' => array(1291)); /* CYRILLIC CAPITAL LETTER KOMI NJE */ -$config['0500_052f'][] = array('upper' => 1292, 'status' => 'C', 'lower' => array(1293)); /* CYRILLIC CAPITAL LETTER KOMI SJE */ -$config['0500_052f'][] = array('upper' => 1294, 'status' => 'C', 'lower' => array(1295)); /* CYRILLIC CAPITAL LETTER KOMI TJE */ -$config['0500_052f'][] = array('upper' => 1296, 'status' => 'C', 'lower' => array(1297)); /* CYRILLIC CAPITAL LETTER ZE */ -$config['0500_052f'][] = array('upper' => 1298, 'status' => 'C', 'lower' => array(1299)); /* CYRILLIC CAPITAL LETTER El with hook */ +$config['0500_052f'][] = ['upper' => 1280, 'status' => 'C', 'lower' => [1281]]; /* CYRILLIC CAPITAL LETTER KOMI DE */ +$config['0500_052f'][] = ['upper' => 1282, 'status' => 'C', 'lower' => [1283]]; /* CYRILLIC CAPITAL LETTER KOMI DJE */ +$config['0500_052f'][] = ['upper' => 1284, 'status' => 'C', 'lower' => [1285]]; /* CYRILLIC CAPITAL LETTER KOMI ZJE */ +$config['0500_052f'][] = ['upper' => 1286, 'status' => 'C', 'lower' => [1287]]; /* CYRILLIC CAPITAL LETTER KOMI DZJE */ +$config['0500_052f'][] = ['upper' => 1288, 'status' => 'C', 'lower' => [1289]]; /* CYRILLIC CAPITAL LETTER KOMI LJE */ +$config['0500_052f'][] = ['upper' => 1290, 'status' => 'C', 'lower' => [1291]]; /* CYRILLIC CAPITAL LETTER KOMI NJE */ +$config['0500_052f'][] = ['upper' => 1292, 'status' => 'C', 'lower' => [1293]]; /* CYRILLIC CAPITAL LETTER KOMI SJE */ +$config['0500_052f'][] = ['upper' => 1294, 'status' => 'C', 'lower' => [1295]]; /* CYRILLIC CAPITAL LETTER KOMI TJE */ +$config['0500_052f'][] = ['upper' => 1296, 'status' => 'C', 'lower' => [1297]]; /* CYRILLIC CAPITAL LETTER ZE */ +$config['0500_052f'][] = ['upper' => 1298, 'status' => 'C', 'lower' => [1299]]; /* CYRILLIC CAPITAL LETTER El with hook */ diff --git a/lib/Cake/Config/unicode/casefolding/0530_058f.php b/lib/Cake/Config/unicode/casefolding/0530_058f.php index 0603db5b..6012c048 100755 --- a/lib/Cake/Config/unicode/casefolding/0530_058f.php +++ b/lib/Cake/Config/unicode/casefolding/0530_058f.php @@ -27,7 +27,7 @@ * * The lower filed is an array of the decimal values that form the lower case version of a character. * - * The status field is: + * The status field is: * C: common case folding, common mappings shared by both simple and full mappings. * F: full case folding, mappings that cause strings to grow in length. Multiple characters are separated by spaces. * S: simple case folding, mappings to single characters where different from F. @@ -37,41 +37,41 @@ * Note that the Turkic mappings do not maintain canonical equivalence without additional processing. * See the discussions of case mapping in the Unicode Standard for more information. */ -$config['0530_058f'][] = array('upper' => 1329, 'status' => 'C', 'lower' => array(1377)); /* ARMENIAN CAPITAL LETTER AYB */ -$config['0530_058f'][] = array('upper' => 1330, 'status' => 'C', 'lower' => array(1378)); /* ARMENIAN CAPITAL LETTER BEN */ -$config['0530_058f'][] = array('upper' => 1331, 'status' => 'C', 'lower' => array(1379)); /* ARMENIAN CAPITAL LETTER GIM */ -$config['0530_058f'][] = array('upper' => 1332, 'status' => 'C', 'lower' => array(1380)); /* ARMENIAN CAPITAL LETTER DA */ -$config['0530_058f'][] = array('upper' => 1333, 'status' => 'C', 'lower' => array(1381)); /* ARMENIAN CAPITAL LETTER ECH */ -$config['0530_058f'][] = array('upper' => 1334, 'status' => 'C', 'lower' => array(1382)); /* ARMENIAN CAPITAL LETTER ZA */ -$config['0530_058f'][] = array('upper' => 1335, 'status' => 'C', 'lower' => array(1383)); /* ARMENIAN CAPITAL LETTER EH */ -$config['0530_058f'][] = array('upper' => 1336, 'status' => 'C', 'lower' => array(1384)); /* ARMENIAN CAPITAL LETTER ET */ -$config['0530_058f'][] = array('upper' => 1337, 'status' => 'C', 'lower' => array(1385)); /* ARMENIAN CAPITAL LETTER TO */ -$config['0530_058f'][] = array('upper' => 1338, 'status' => 'C', 'lower' => array(1386)); /* ARMENIAN CAPITAL LETTER ZHE */ -$config['0530_058f'][] = array('upper' => 1339, 'status' => 'C', 'lower' => array(1387)); /* ARMENIAN CAPITAL LETTER INI */ -$config['0530_058f'][] = array('upper' => 1340, 'status' => 'C', 'lower' => array(1388)); /* ARMENIAN CAPITAL LETTER LIWN */ -$config['0530_058f'][] = array('upper' => 1341, 'status' => 'C', 'lower' => array(1389)); /* ARMENIAN CAPITAL LETTER XEH */ -$config['0530_058f'][] = array('upper' => 1342, 'status' => 'C', 'lower' => array(1390)); /* ARMENIAN CAPITAL LETTER CA */ -$config['0530_058f'][] = array('upper' => 1343, 'status' => 'C', 'lower' => array(1391)); /* ARMENIAN CAPITAL LETTER KEN */ -$config['0530_058f'][] = array('upper' => 1344, 'status' => 'C', 'lower' => array(1392)); /* ARMENIAN CAPITAL LETTER HO */ -$config['0530_058f'][] = array('upper' => 1345, 'status' => 'C', 'lower' => array(1393)); /* ARMENIAN CAPITAL LETTER JA */ -$config['0530_058f'][] = array('upper' => 1346, 'status' => 'C', 'lower' => array(1394)); /* ARMENIAN CAPITAL LETTER GHAD */ -$config['0530_058f'][] = array('upper' => 1347, 'status' => 'C', 'lower' => array(1395)); /* ARMENIAN CAPITAL LETTER CHEH */ -$config['0530_058f'][] = array('upper' => 1348, 'status' => 'C', 'lower' => array(1396)); /* ARMENIAN CAPITAL LETTER MEN */ -$config['0530_058f'][] = array('upper' => 1349, 'status' => 'C', 'lower' => array(1397)); /* ARMENIAN CAPITAL LETTER YI */ -$config['0530_058f'][] = array('upper' => 1350, 'status' => 'C', 'lower' => array(1398)); /* ARMENIAN CAPITAL LETTER NOW */ -$config['0530_058f'][] = array('upper' => 1351, 'status' => 'C', 'lower' => array(1399)); /* ARMENIAN CAPITAL LETTER SHA */ -$config['0530_058f'][] = array('upper' => 1352, 'status' => 'C', 'lower' => array(1400)); /* ARMENIAN CAPITAL LETTER VO */ -$config['0530_058f'][] = array('upper' => 1353, 'status' => 'C', 'lower' => array(1401)); /* ARMENIAN CAPITAL LETTER CHA */ -$config['0530_058f'][] = array('upper' => 1354, 'status' => 'C', 'lower' => array(1402)); /* ARMENIAN CAPITAL LETTER PEH */ -$config['0530_058f'][] = array('upper' => 1355, 'status' => 'C', 'lower' => array(1403)); /* ARMENIAN CAPITAL LETTER JHEH */ -$config['0530_058f'][] = array('upper' => 1356, 'status' => 'C', 'lower' => array(1404)); /* ARMENIAN CAPITAL LETTER RA */ -$config['0530_058f'][] = array('upper' => 1357, 'status' => 'C', 'lower' => array(1405)); /* ARMENIAN CAPITAL LETTER SEH */ -$config['0530_058f'][] = array('upper' => 1358, 'status' => 'C', 'lower' => array(1406)); /* ARMENIAN CAPITAL LETTER VEW */ -$config['0530_058f'][] = array('upper' => 1359, 'status' => 'C', 'lower' => array(1407)); /* ARMENIAN CAPITAL LETTER TIWN */ -$config['0530_058f'][] = array('upper' => 1360, 'status' => 'C', 'lower' => array(1408)); /* ARMENIAN CAPITAL LETTER REH */ -$config['0530_058f'][] = array('upper' => 1361, 'status' => 'C', 'lower' => array(1409)); /* ARMENIAN CAPITAL LETTER CO */ -$config['0530_058f'][] = array('upper' => 1362, 'status' => 'C', 'lower' => array(1410)); /* ARMENIAN CAPITAL LETTER YIWN */ -$config['0530_058f'][] = array('upper' => 1363, 'status' => 'C', 'lower' => array(1411)); /* ARMENIAN CAPITAL LETTER PIWR */ -$config['0530_058f'][] = array('upper' => 1364, 'status' => 'C', 'lower' => array(1412)); /* ARMENIAN CAPITAL LETTER KEH */ -$config['0530_058f'][] = array('upper' => 1365, 'status' => 'C', 'lower' => array(1413)); /* ARMENIAN CAPITAL LETTER OH */ -$config['0530_058f'][] = array('upper' => 1366, 'status' => 'C', 'lower' => array(1414)); /* ARMENIAN CAPITAL LETTER FEH */ +$config['0530_058f'][] = ['upper' => 1329, 'status' => 'C', 'lower' => [1377]]; /* ARMENIAN CAPITAL LETTER AYB */ +$config['0530_058f'][] = ['upper' => 1330, 'status' => 'C', 'lower' => [1378]]; /* ARMENIAN CAPITAL LETTER BEN */ +$config['0530_058f'][] = ['upper' => 1331, 'status' => 'C', 'lower' => [1379]]; /* ARMENIAN CAPITAL LETTER GIM */ +$config['0530_058f'][] = ['upper' => 1332, 'status' => 'C', 'lower' => [1380]]; /* ARMENIAN CAPITAL LETTER DA */ +$config['0530_058f'][] = ['upper' => 1333, 'status' => 'C', 'lower' => [1381]]; /* ARMENIAN CAPITAL LETTER ECH */ +$config['0530_058f'][] = ['upper' => 1334, 'status' => 'C', 'lower' => [1382]]; /* ARMENIAN CAPITAL LETTER ZA */ +$config['0530_058f'][] = ['upper' => 1335, 'status' => 'C', 'lower' => [1383]]; /* ARMENIAN CAPITAL LETTER EH */ +$config['0530_058f'][] = ['upper' => 1336, 'status' => 'C', 'lower' => [1384]]; /* ARMENIAN CAPITAL LETTER ET */ +$config['0530_058f'][] = ['upper' => 1337, 'status' => 'C', 'lower' => [1385]]; /* ARMENIAN CAPITAL LETTER TO */ +$config['0530_058f'][] = ['upper' => 1338, 'status' => 'C', 'lower' => [1386]]; /* ARMENIAN CAPITAL LETTER ZHE */ +$config['0530_058f'][] = ['upper' => 1339, 'status' => 'C', 'lower' => [1387]]; /* ARMENIAN CAPITAL LETTER INI */ +$config['0530_058f'][] = ['upper' => 1340, 'status' => 'C', 'lower' => [1388]]; /* ARMENIAN CAPITAL LETTER LIWN */ +$config['0530_058f'][] = ['upper' => 1341, 'status' => 'C', 'lower' => [1389]]; /* ARMENIAN CAPITAL LETTER XEH */ +$config['0530_058f'][] = ['upper' => 1342, 'status' => 'C', 'lower' => [1390]]; /* ARMENIAN CAPITAL LETTER CA */ +$config['0530_058f'][] = ['upper' => 1343, 'status' => 'C', 'lower' => [1391]]; /* ARMENIAN CAPITAL LETTER KEN */ +$config['0530_058f'][] = ['upper' => 1344, 'status' => 'C', 'lower' => [1392]]; /* ARMENIAN CAPITAL LETTER HO */ +$config['0530_058f'][] = ['upper' => 1345, 'status' => 'C', 'lower' => [1393]]; /* ARMENIAN CAPITAL LETTER JA */ +$config['0530_058f'][] = ['upper' => 1346, 'status' => 'C', 'lower' => [1394]]; /* ARMENIAN CAPITAL LETTER GHAD */ +$config['0530_058f'][] = ['upper' => 1347, 'status' => 'C', 'lower' => [1395]]; /* ARMENIAN CAPITAL LETTER CHEH */ +$config['0530_058f'][] = ['upper' => 1348, 'status' => 'C', 'lower' => [1396]]; /* ARMENIAN CAPITAL LETTER MEN */ +$config['0530_058f'][] = ['upper' => 1349, 'status' => 'C', 'lower' => [1397]]; /* ARMENIAN CAPITAL LETTER YI */ +$config['0530_058f'][] = ['upper' => 1350, 'status' => 'C', 'lower' => [1398]]; /* ARMENIAN CAPITAL LETTER NOW */ +$config['0530_058f'][] = ['upper' => 1351, 'status' => 'C', 'lower' => [1399]]; /* ARMENIAN CAPITAL LETTER SHA */ +$config['0530_058f'][] = ['upper' => 1352, 'status' => 'C', 'lower' => [1400]]; /* ARMENIAN CAPITAL LETTER VO */ +$config['0530_058f'][] = ['upper' => 1353, 'status' => 'C', 'lower' => [1401]]; /* ARMENIAN CAPITAL LETTER CHA */ +$config['0530_058f'][] = ['upper' => 1354, 'status' => 'C', 'lower' => [1402]]; /* ARMENIAN CAPITAL LETTER PEH */ +$config['0530_058f'][] = ['upper' => 1355, 'status' => 'C', 'lower' => [1403]]; /* ARMENIAN CAPITAL LETTER JHEH */ +$config['0530_058f'][] = ['upper' => 1356, 'status' => 'C', 'lower' => [1404]]; /* ARMENIAN CAPITAL LETTER RA */ +$config['0530_058f'][] = ['upper' => 1357, 'status' => 'C', 'lower' => [1405]]; /* ARMENIAN CAPITAL LETTER SEH */ +$config['0530_058f'][] = ['upper' => 1358, 'status' => 'C', 'lower' => [1406]]; /* ARMENIAN CAPITAL LETTER VEW */ +$config['0530_058f'][] = ['upper' => 1359, 'status' => 'C', 'lower' => [1407]]; /* ARMENIAN CAPITAL LETTER TIWN */ +$config['0530_058f'][] = ['upper' => 1360, 'status' => 'C', 'lower' => [1408]]; /* ARMENIAN CAPITAL LETTER REH */ +$config['0530_058f'][] = ['upper' => 1361, 'status' => 'C', 'lower' => [1409]]; /* ARMENIAN CAPITAL LETTER CO */ +$config['0530_058f'][] = ['upper' => 1362, 'status' => 'C', 'lower' => [1410]]; /* ARMENIAN CAPITAL LETTER YIWN */ +$config['0530_058f'][] = ['upper' => 1363, 'status' => 'C', 'lower' => [1411]]; /* ARMENIAN CAPITAL LETTER PIWR */ +$config['0530_058f'][] = ['upper' => 1364, 'status' => 'C', 'lower' => [1412]]; /* ARMENIAN CAPITAL LETTER KEH */ +$config['0530_058f'][] = ['upper' => 1365, 'status' => 'C', 'lower' => [1413]]; /* ARMENIAN CAPITAL LETTER OH */ +$config['0530_058f'][] = ['upper' => 1366, 'status' => 'C', 'lower' => [1414]]; /* ARMENIAN CAPITAL LETTER FEH */ diff --git a/lib/Cake/Config/unicode/casefolding/1e00_1eff.php b/lib/Cake/Config/unicode/casefolding/1e00_1eff.php index 6709c677..58bf554d 100755 --- a/lib/Cake/Config/unicode/casefolding/1e00_1eff.php +++ b/lib/Cake/Config/unicode/casefolding/1e00_1eff.php @@ -27,7 +27,7 @@ * * The lower filed is an array of the decimal values that form the lower case version of a character. * - * The status field is: + * The status field is: * C: common case folding, common mappings shared by both simple and full mappings. * F: full case folding, mappings that cause strings to grow in length. Multiple characters are separated by spaces. * S: simple case folding, mappings to single characters where different from F. @@ -37,81 +37,81 @@ * Note that the Turkic mappings do not maintain canonical equivalence without additional processing. * See the discussions of case mapping in the Unicode Standard for more information. */ -$config['1e00_1eff'][] = array('upper' => 7680, 'status' => 'C', 'lower' => array(7681)); /* LATIN CAPITAL LETTER A WITH RING BELOW */ -$config['1e00_1eff'][] = array('upper' => 7682, 'status' => 'C', 'lower' => array(7683)); /* LATIN CAPITAL LETTER B WITH DOT ABOVE */ -$config['1e00_1eff'][] = array('upper' => 7684, 'status' => 'C', 'lower' => array(7685)); /* LATIN CAPITAL LETTER B WITH DOT BELOW */ -$config['1e00_1eff'][] = array('upper' => 7686, 'status' => 'C', 'lower' => array(7687)); /* LATIN CAPITAL LETTER B WITH LINE BELOW */ -$config['1e00_1eff'][] = array('upper' => 7688, 'status' => 'C', 'lower' => array(7689)); /* LATIN CAPITAL LETTER C WITH CEDILLA AND ACUTE */ -$config['1e00_1eff'][] = array('upper' => 7690, 'status' => 'C', 'lower' => array(7691)); /* LATIN CAPITAL LETTER D WITH DOT ABOVE */ -$config['1e00_1eff'][] = array('upper' => 7692, 'status' => 'C', 'lower' => array(7693)); /* LATIN CAPITAL LETTER D WITH DOT BELOW */ -$config['1e00_1eff'][] = array('upper' => 7694, 'status' => 'C', 'lower' => array(7695)); /* LATIN CAPITAL LETTER D WITH LINE BELOW */ -$config['1e00_1eff'][] = array('upper' => 7696, 'status' => 'C', 'lower' => array(7697)); /* LATIN CAPITAL LETTER D WITH CEDILLA */ -$config['1e00_1eff'][] = array('upper' => 7698, 'status' => 'C', 'lower' => array(7699)); /* LATIN CAPITAL LETTER D WITH CIRCUMFLEX BELOW */ -$config['1e00_1eff'][] = array('upper' => 7700, 'status' => 'C', 'lower' => array(7701)); /* LATIN CAPITAL LETTER E WITH MACRON AND GRAVE */ -$config['1e00_1eff'][] = array('upper' => 7702, 'status' => 'C', 'lower' => array(7703)); /* LATIN CAPITAL LETTER E WITH MACRON AND ACUTE */ -$config['1e00_1eff'][] = array('upper' => 7704, 'status' => 'C', 'lower' => array(7705)); /* LATIN CAPITAL LETTER E WITH CIRCUMFLEX BELOW */ -$config['1e00_1eff'][] = array('upper' => 7706, 'status' => 'C', 'lower' => array(7707)); /* LATIN CAPITAL LETTER E WITH TILDE BELOW */ -$config['1e00_1eff'][] = array('upper' => 7708, 'status' => 'C', 'lower' => array(7709)); /* LATIN CAPITAL LETTER E WITH CEDILLA AND BREVE */ -$config['1e00_1eff'][] = array('upper' => 7710, 'status' => 'C', 'lower' => array(7711)); /* LATIN CAPITAL LETTER F WITH DOT ABOVE */ -$config['1e00_1eff'][] = array('upper' => 7712, 'status' => 'C', 'lower' => array(7713)); /* LATIN CAPITAL LETTER G WITH MACRON */ -$config['1e00_1eff'][] = array('upper' => 7714, 'status' => 'C', 'lower' => array(7715)); /* LATIN CAPITAL LETTER H WITH DOT ABOVE */ -$config['1e00_1eff'][] = array('upper' => 7716, 'status' => 'C', 'lower' => array(7717)); /* LATIN CAPITAL LETTER H WITH DOT BELOW */ -$config['1e00_1eff'][] = array('upper' => 7718, 'status' => 'C', 'lower' => array(7719)); /* LATIN CAPITAL LETTER H WITH DIAERESIS */ -$config['1e00_1eff'][] = array('upper' => 7720, 'status' => 'C', 'lower' => array(7721)); /* LATIN CAPITAL LETTER H WITH CEDILLA */ -$config['1e00_1eff'][] = array('upper' => 7722, 'status' => 'C', 'lower' => array(7723)); /* LATIN CAPITAL LETTER H WITH BREVE BELOW */ -$config['1e00_1eff'][] = array('upper' => 7724, 'status' => 'C', 'lower' => array(7725)); /* LATIN CAPITAL LETTER I WITH TILDE BELOW */ -$config['1e00_1eff'][] = array('upper' => 7726, 'status' => 'C', 'lower' => array(7727)); /* LATIN CAPITAL LETTER I WITH DIAERESIS AND ACUTE */ -$config['1e00_1eff'][] = array('upper' => 7728, 'status' => 'C', 'lower' => array(7729)); /* LATIN CAPITAL LETTER K WITH ACUTE */ -$config['1e00_1eff'][] = array('upper' => 7730, 'status' => 'C', 'lower' => array(7731)); /* LATIN CAPITAL LETTER K WITH DOT BELOW */ -$config['1e00_1eff'][] = array('upper' => 7732, 'status' => 'C', 'lower' => array(7733)); /* LATIN CAPITAL LETTER K WITH LINE BELOW */ -$config['1e00_1eff'][] = array('upper' => 7734, 'status' => 'C', 'lower' => array(7735)); /* LATIN CAPITAL LETTER L WITH DOT BELOW */ -$config['1e00_1eff'][] = array('upper' => 7736, 'status' => 'C', 'lower' => array(7737)); /* LATIN CAPITAL LETTER L WITH DOT BELOW AND MACRON */ -$config['1e00_1eff'][] = array('upper' => 7738, 'status' => 'C', 'lower' => array(7739)); /* LATIN CAPITAL LETTER L WITH LINE BELOW */ -$config['1e00_1eff'][] = array('upper' => 7740, 'status' => 'C', 'lower' => array(7741)); /* LATIN CAPITAL LETTER L WITH CIRCUMFLEX BELOW */ -$config['1e00_1eff'][] = array('upper' => 7742, 'status' => 'C', 'lower' => array(7743)); /* LATIN CAPITAL LETTER M WITH ACUTE */ -$config['1e00_1eff'][] = array('upper' => 7744, 'status' => 'C', 'lower' => array(7745)); /* LATIN CAPITAL LETTER M WITH DOT ABOVE */ -$config['1e00_1eff'][] = array('upper' => 7746, 'status' => 'C', 'lower' => array(7747)); /* LATIN CAPITAL LETTER M WITH DOT BELOW */ -$config['1e00_1eff'][] = array('upper' => 7748, 'status' => 'C', 'lower' => array(7749)); /* LATIN CAPITAL LETTER N WITH DOT ABOVE */ -$config['1e00_1eff'][] = array('upper' => 7750, 'status' => 'C', 'lower' => array(7751)); /* LATIN CAPITAL LETTER N WITH DOT BELOW */ -$config['1e00_1eff'][] = array('upper' => 7752, 'status' => 'C', 'lower' => array(7753)); /* LATIN CAPITAL LETTER N WITH LINE BELOW */ -$config['1e00_1eff'][] = array('upper' => 7754, 'status' => 'C', 'lower' => array(7755)); /* LATIN CAPITAL LETTER N WITH CIRCUMFLEX BELOW */ -$config['1e00_1eff'][] = array('upper' => 7756, 'status' => 'C', 'lower' => array(7757)); /* LATIN CAPITAL LETTER O WITH TILDE AND ACUTE */ -$config['1e00_1eff'][] = array('upper' => 7758, 'status' => 'C', 'lower' => array(7759)); /* LATIN CAPITAL LETTER O WITH TILDE AND DIAERESIS */ -$config['1e00_1eff'][] = array('upper' => 7760, 'status' => 'C', 'lower' => array(7761)); /* LATIN CAPITAL LETTER O WITH MACRON AND GRAVE */ -$config['1e00_1eff'][] = array('upper' => 7762, 'status' => 'C', 'lower' => array(7763)); /* LATIN CAPITAL LETTER O WITH MACRON AND ACUTE */ -$config['1e00_1eff'][] = array('upper' => 7764, 'status' => 'C', 'lower' => array(7765)); /* LATIN CAPITAL LETTER P WITH ACUTE */ -$config['1e00_1eff'][] = array('upper' => 7766, 'status' => 'C', 'lower' => array(7767)); /* LATIN CAPITAL LETTER P WITH DOT ABOVE */ -$config['1e00_1eff'][] = array('upper' => 7768, 'status' => 'C', 'lower' => array(7769)); /* LATIN CAPITAL LETTER R WITH DOT ABOVE */ -$config['1e00_1eff'][] = array('upper' => 7770, 'status' => 'C', 'lower' => array(7771)); /* LATIN CAPITAL LETTER R WITH DOT BELOW */ -$config['1e00_1eff'][] = array('upper' => 7772, 'status' => 'C', 'lower' => array(7773)); /* LATIN CAPITAL LETTER R WITH DOT BELOW AND MACRON */ -$config['1e00_1eff'][] = array('upper' => 7774, 'status' => 'C', 'lower' => array(7775)); /* LATIN CAPITAL LETTER R WITH LINE BELOW */ -$config['1e00_1eff'][] = array('upper' => 7776, 'status' => 'C', 'lower' => array(7777)); /* LATIN CAPITAL LETTER S WITH DOT ABOVE */ -$config['1e00_1eff'][] = array('upper' => 7778, 'status' => 'C', 'lower' => array(7779)); /* LATIN CAPITAL LETTER S WITH DOT BELOW */ -$config['1e00_1eff'][] = array('upper' => 7780, 'status' => 'C', 'lower' => array(7781)); /* LATIN CAPITAL LETTER S WITH ACUTE AND DOT ABOVE */ -$config['1e00_1eff'][] = array('upper' => 7782, 'status' => 'C', 'lower' => array(7783)); /* LATIN CAPITAL LETTER S WITH CARON AND DOT ABOVE */ -$config['1e00_1eff'][] = array('upper' => 7784, 'status' => 'C', 'lower' => array(7785)); /* LATIN CAPITAL LETTER S WITH DOT BELOW AND DOT ABOVE */ -$config['1e00_1eff'][] = array('upper' => 7786, 'status' => 'C', 'lower' => array(7787)); /* LATIN CAPITAL LETTER T WITH DOT ABOVE */ -$config['1e00_1eff'][] = array('upper' => 7788, 'status' => 'C', 'lower' => array(7789)); /* LATIN CAPITAL LETTER T WITH DOT BELOW */ -$config['1e00_1eff'][] = array('upper' => 7790, 'status' => 'C', 'lower' => array(7791)); /* LATIN CAPITAL LETTER T WITH LINE BELOW */ -$config['1e00_1eff'][] = array('upper' => 7792, 'status' => 'C', 'lower' => array(7793)); /* LATIN CAPITAL LETTER T WITH CIRCUMFLEX BELOW */ -$config['1e00_1eff'][] = array('upper' => 7794, 'status' => 'C', 'lower' => array(7795)); /* LATIN CAPITAL LETTER U WITH DIAERESIS BELOW */ -$config['1e00_1eff'][] = array('upper' => 7796, 'status' => 'C', 'lower' => array(7797)); /* LATIN CAPITAL LETTER U WITH TILDE BELOW */ -$config['1e00_1eff'][] = array('upper' => 7798, 'status' => 'C', 'lower' => array(7799)); /* LATIN CAPITAL LETTER U WITH CIRCUMFLEX BELOW */ -$config['1e00_1eff'][] = array('upper' => 7800, 'status' => 'C', 'lower' => array(7801)); /* LATIN CAPITAL LETTER U WITH TILDE AND ACUTE */ -$config['1e00_1eff'][] = array('upper' => 7802, 'status' => 'C', 'lower' => array(7803)); /* LATIN CAPITAL LETTER U WITH MACRON AND DIAERESIS */ -$config['1e00_1eff'][] = array('upper' => 7804, 'status' => 'C', 'lower' => array(7805)); /* LATIN CAPITAL LETTER V WITH TILDE */ -$config['1e00_1eff'][] = array('upper' => 7806, 'status' => 'C', 'lower' => array(7807)); /* LATIN CAPITAL LETTER V WITH DOT BELOW */ -$config['1e00_1eff'][] = array('upper' => 7808, 'status' => 'C', 'lower' => array(7809)); /* LATIN CAPITAL LETTER W WITH GRAVE */ -$config['1e00_1eff'][] = array('upper' => 7810, 'status' => 'C', 'lower' => array(7811)); /* LATIN CAPITAL LETTER W WITH ACUTE */ -$config['1e00_1eff'][] = array('upper' => 7812, 'status' => 'C', 'lower' => array(7813)); /* LATIN CAPITAL LETTER W WITH DIAERESIS */ -$config['1e00_1eff'][] = array('upper' => 7814, 'status' => 'C', 'lower' => array(7815)); /* LATIN CAPITAL LETTER W WITH DOT ABOVE */ -$config['1e00_1eff'][] = array('upper' => 7816, 'status' => 'C', 'lower' => array(7817)); /* LATIN CAPITAL LETTER W WITH DOT BELOW */ -$config['1e00_1eff'][] = array('upper' => 7818, 'status' => 'C', 'lower' => array(7819)); /* LATIN CAPITAL LETTER X WITH DOT ABOVE */ -$config['1e00_1eff'][] = array('upper' => 7820, 'status' => 'C', 'lower' => array(7821)); /* LATIN CAPITAL LETTER X WITH DIAERESIS */ -$config['1e00_1eff'][] = array('upper' => 7822, 'status' => 'C', 'lower' => array(7823)); /* LATIN CAPITAL LETTER Y WITH DOT ABOVE */ -$config['1e00_1eff'][] = array('upper' => 7824, 'status' => 'C', 'lower' => array(7825)); /* LATIN CAPITAL LETTER Z WITH CIRCUMFLEX */ -$config['1e00_1eff'][] = array('upper' => 7826, 'status' => 'C', 'lower' => array(7827)); /* LATIN CAPITAL LETTER Z WITH DOT BELOW */ -$config['1e00_1eff'][] = array('upper' => 7828, 'status' => 'C', 'lower' => array(7829)); /* LATIN CAPITAL LETTER Z WITH LINE BELOW */ +$config['1e00_1eff'][] = ['upper' => 7680, 'status' => 'C', 'lower' => [7681]]; /* LATIN CAPITAL LETTER A WITH RING BELOW */ +$config['1e00_1eff'][] = ['upper' => 7682, 'status' => 'C', 'lower' => [7683]]; /* LATIN CAPITAL LETTER B WITH DOT ABOVE */ +$config['1e00_1eff'][] = ['upper' => 7684, 'status' => 'C', 'lower' => [7685]]; /* LATIN CAPITAL LETTER B WITH DOT BELOW */ +$config['1e00_1eff'][] = ['upper' => 7686, 'status' => 'C', 'lower' => [7687]]; /* LATIN CAPITAL LETTER B WITH LINE BELOW */ +$config['1e00_1eff'][] = ['upper' => 7688, 'status' => 'C', 'lower' => [7689]]; /* LATIN CAPITAL LETTER C WITH CEDILLA AND ACUTE */ +$config['1e00_1eff'][] = ['upper' => 7690, 'status' => 'C', 'lower' => [7691]]; /* LATIN CAPITAL LETTER D WITH DOT ABOVE */ +$config['1e00_1eff'][] = ['upper' => 7692, 'status' => 'C', 'lower' => [7693]]; /* LATIN CAPITAL LETTER D WITH DOT BELOW */ +$config['1e00_1eff'][] = ['upper' => 7694, 'status' => 'C', 'lower' => [7695]]; /* LATIN CAPITAL LETTER D WITH LINE BELOW */ +$config['1e00_1eff'][] = ['upper' => 7696, 'status' => 'C', 'lower' => [7697]]; /* LATIN CAPITAL LETTER D WITH CEDILLA */ +$config['1e00_1eff'][] = ['upper' => 7698, 'status' => 'C', 'lower' => [7699]]; /* LATIN CAPITAL LETTER D WITH CIRCUMFLEX BELOW */ +$config['1e00_1eff'][] = ['upper' => 7700, 'status' => 'C', 'lower' => [7701]]; /* LATIN CAPITAL LETTER E WITH MACRON AND GRAVE */ +$config['1e00_1eff'][] = ['upper' => 7702, 'status' => 'C', 'lower' => [7703]]; /* LATIN CAPITAL LETTER E WITH MACRON AND ACUTE */ +$config['1e00_1eff'][] = ['upper' => 7704, 'status' => 'C', 'lower' => [7705]]; /* LATIN CAPITAL LETTER E WITH CIRCUMFLEX BELOW */ +$config['1e00_1eff'][] = ['upper' => 7706, 'status' => 'C', 'lower' => [7707]]; /* LATIN CAPITAL LETTER E WITH TILDE BELOW */ +$config['1e00_1eff'][] = ['upper' => 7708, 'status' => 'C', 'lower' => [7709]]; /* LATIN CAPITAL LETTER E WITH CEDILLA AND BREVE */ +$config['1e00_1eff'][] = ['upper' => 7710, 'status' => 'C', 'lower' => [7711]]; /* LATIN CAPITAL LETTER F WITH DOT ABOVE */ +$config['1e00_1eff'][] = ['upper' => 7712, 'status' => 'C', 'lower' => [7713]]; /* LATIN CAPITAL LETTER G WITH MACRON */ +$config['1e00_1eff'][] = ['upper' => 7714, 'status' => 'C', 'lower' => [7715]]; /* LATIN CAPITAL LETTER H WITH DOT ABOVE */ +$config['1e00_1eff'][] = ['upper' => 7716, 'status' => 'C', 'lower' => [7717]]; /* LATIN CAPITAL LETTER H WITH DOT BELOW */ +$config['1e00_1eff'][] = ['upper' => 7718, 'status' => 'C', 'lower' => [7719]]; /* LATIN CAPITAL LETTER H WITH DIAERESIS */ +$config['1e00_1eff'][] = ['upper' => 7720, 'status' => 'C', 'lower' => [7721]]; /* LATIN CAPITAL LETTER H WITH CEDILLA */ +$config['1e00_1eff'][] = ['upper' => 7722, 'status' => 'C', 'lower' => [7723]]; /* LATIN CAPITAL LETTER H WITH BREVE BELOW */ +$config['1e00_1eff'][] = ['upper' => 7724, 'status' => 'C', 'lower' => [7725]]; /* LATIN CAPITAL LETTER I WITH TILDE BELOW */ +$config['1e00_1eff'][] = ['upper' => 7726, 'status' => 'C', 'lower' => [7727]]; /* LATIN CAPITAL LETTER I WITH DIAERESIS AND ACUTE */ +$config['1e00_1eff'][] = ['upper' => 7728, 'status' => 'C', 'lower' => [7729]]; /* LATIN CAPITAL LETTER K WITH ACUTE */ +$config['1e00_1eff'][] = ['upper' => 7730, 'status' => 'C', 'lower' => [7731]]; /* LATIN CAPITAL LETTER K WITH DOT BELOW */ +$config['1e00_1eff'][] = ['upper' => 7732, 'status' => 'C', 'lower' => [7733]]; /* LATIN CAPITAL LETTER K WITH LINE BELOW */ +$config['1e00_1eff'][] = ['upper' => 7734, 'status' => 'C', 'lower' => [7735]]; /* LATIN CAPITAL LETTER L WITH DOT BELOW */ +$config['1e00_1eff'][] = ['upper' => 7736, 'status' => 'C', 'lower' => [7737]]; /* LATIN CAPITAL LETTER L WITH DOT BELOW AND MACRON */ +$config['1e00_1eff'][] = ['upper' => 7738, 'status' => 'C', 'lower' => [7739]]; /* LATIN CAPITAL LETTER L WITH LINE BELOW */ +$config['1e00_1eff'][] = ['upper' => 7740, 'status' => 'C', 'lower' => [7741]]; /* LATIN CAPITAL LETTER L WITH CIRCUMFLEX BELOW */ +$config['1e00_1eff'][] = ['upper' => 7742, 'status' => 'C', 'lower' => [7743]]; /* LATIN CAPITAL LETTER M WITH ACUTE */ +$config['1e00_1eff'][] = ['upper' => 7744, 'status' => 'C', 'lower' => [7745]]; /* LATIN CAPITAL LETTER M WITH DOT ABOVE */ +$config['1e00_1eff'][] = ['upper' => 7746, 'status' => 'C', 'lower' => [7747]]; /* LATIN CAPITAL LETTER M WITH DOT BELOW */ +$config['1e00_1eff'][] = ['upper' => 7748, 'status' => 'C', 'lower' => [7749]]; /* LATIN CAPITAL LETTER N WITH DOT ABOVE */ +$config['1e00_1eff'][] = ['upper' => 7750, 'status' => 'C', 'lower' => [7751]]; /* LATIN CAPITAL LETTER N WITH DOT BELOW */ +$config['1e00_1eff'][] = ['upper' => 7752, 'status' => 'C', 'lower' => [7753]]; /* LATIN CAPITAL LETTER N WITH LINE BELOW */ +$config['1e00_1eff'][] = ['upper' => 7754, 'status' => 'C', 'lower' => [7755]]; /* LATIN CAPITAL LETTER N WITH CIRCUMFLEX BELOW */ +$config['1e00_1eff'][] = ['upper' => 7756, 'status' => 'C', 'lower' => [7757]]; /* LATIN CAPITAL LETTER O WITH TILDE AND ACUTE */ +$config['1e00_1eff'][] = ['upper' => 7758, 'status' => 'C', 'lower' => [7759]]; /* LATIN CAPITAL LETTER O WITH TILDE AND DIAERESIS */ +$config['1e00_1eff'][] = ['upper' => 7760, 'status' => 'C', 'lower' => [7761]]; /* LATIN CAPITAL LETTER O WITH MACRON AND GRAVE */ +$config['1e00_1eff'][] = ['upper' => 7762, 'status' => 'C', 'lower' => [7763]]; /* LATIN CAPITAL LETTER O WITH MACRON AND ACUTE */ +$config['1e00_1eff'][] = ['upper' => 7764, 'status' => 'C', 'lower' => [7765]]; /* LATIN CAPITAL LETTER P WITH ACUTE */ +$config['1e00_1eff'][] = ['upper' => 7766, 'status' => 'C', 'lower' => [7767]]; /* LATIN CAPITAL LETTER P WITH DOT ABOVE */ +$config['1e00_1eff'][] = ['upper' => 7768, 'status' => 'C', 'lower' => [7769]]; /* LATIN CAPITAL LETTER R WITH DOT ABOVE */ +$config['1e00_1eff'][] = ['upper' => 7770, 'status' => 'C', 'lower' => [7771]]; /* LATIN CAPITAL LETTER R WITH DOT BELOW */ +$config['1e00_1eff'][] = ['upper' => 7772, 'status' => 'C', 'lower' => [7773]]; /* LATIN CAPITAL LETTER R WITH DOT BELOW AND MACRON */ +$config['1e00_1eff'][] = ['upper' => 7774, 'status' => 'C', 'lower' => [7775]]; /* LATIN CAPITAL LETTER R WITH LINE BELOW */ +$config['1e00_1eff'][] = ['upper' => 7776, 'status' => 'C', 'lower' => [7777]]; /* LATIN CAPITAL LETTER S WITH DOT ABOVE */ +$config['1e00_1eff'][] = ['upper' => 7778, 'status' => 'C', 'lower' => [7779]]; /* LATIN CAPITAL LETTER S WITH DOT BELOW */ +$config['1e00_1eff'][] = ['upper' => 7780, 'status' => 'C', 'lower' => [7781]]; /* LATIN CAPITAL LETTER S WITH ACUTE AND DOT ABOVE */ +$config['1e00_1eff'][] = ['upper' => 7782, 'status' => 'C', 'lower' => [7783]]; /* LATIN CAPITAL LETTER S WITH CARON AND DOT ABOVE */ +$config['1e00_1eff'][] = ['upper' => 7784, 'status' => 'C', 'lower' => [7785]]; /* LATIN CAPITAL LETTER S WITH DOT BELOW AND DOT ABOVE */ +$config['1e00_1eff'][] = ['upper' => 7786, 'status' => 'C', 'lower' => [7787]]; /* LATIN CAPITAL LETTER T WITH DOT ABOVE */ +$config['1e00_1eff'][] = ['upper' => 7788, 'status' => 'C', 'lower' => [7789]]; /* LATIN CAPITAL LETTER T WITH DOT BELOW */ +$config['1e00_1eff'][] = ['upper' => 7790, 'status' => 'C', 'lower' => [7791]]; /* LATIN CAPITAL LETTER T WITH LINE BELOW */ +$config['1e00_1eff'][] = ['upper' => 7792, 'status' => 'C', 'lower' => [7793]]; /* LATIN CAPITAL LETTER T WITH CIRCUMFLEX BELOW */ +$config['1e00_1eff'][] = ['upper' => 7794, 'status' => 'C', 'lower' => [7795]]; /* LATIN CAPITAL LETTER U WITH DIAERESIS BELOW */ +$config['1e00_1eff'][] = ['upper' => 7796, 'status' => 'C', 'lower' => [7797]]; /* LATIN CAPITAL LETTER U WITH TILDE BELOW */ +$config['1e00_1eff'][] = ['upper' => 7798, 'status' => 'C', 'lower' => [7799]]; /* LATIN CAPITAL LETTER U WITH CIRCUMFLEX BELOW */ +$config['1e00_1eff'][] = ['upper' => 7800, 'status' => 'C', 'lower' => [7801]]; /* LATIN CAPITAL LETTER U WITH TILDE AND ACUTE */ +$config['1e00_1eff'][] = ['upper' => 7802, 'status' => 'C', 'lower' => [7803]]; /* LATIN CAPITAL LETTER U WITH MACRON AND DIAERESIS */ +$config['1e00_1eff'][] = ['upper' => 7804, 'status' => 'C', 'lower' => [7805]]; /* LATIN CAPITAL LETTER V WITH TILDE */ +$config['1e00_1eff'][] = ['upper' => 7806, 'status' => 'C', 'lower' => [7807]]; /* LATIN CAPITAL LETTER V WITH DOT BELOW */ +$config['1e00_1eff'][] = ['upper' => 7808, 'status' => 'C', 'lower' => [7809]]; /* LATIN CAPITAL LETTER W WITH GRAVE */ +$config['1e00_1eff'][] = ['upper' => 7810, 'status' => 'C', 'lower' => [7811]]; /* LATIN CAPITAL LETTER W WITH ACUTE */ +$config['1e00_1eff'][] = ['upper' => 7812, 'status' => 'C', 'lower' => [7813]]; /* LATIN CAPITAL LETTER W WITH DIAERESIS */ +$config['1e00_1eff'][] = ['upper' => 7814, 'status' => 'C', 'lower' => [7815]]; /* LATIN CAPITAL LETTER W WITH DOT ABOVE */ +$config['1e00_1eff'][] = ['upper' => 7816, 'status' => 'C', 'lower' => [7817]]; /* LATIN CAPITAL LETTER W WITH DOT BELOW */ +$config['1e00_1eff'][] = ['upper' => 7818, 'status' => 'C', 'lower' => [7819]]; /* LATIN CAPITAL LETTER X WITH DOT ABOVE */ +$config['1e00_1eff'][] = ['upper' => 7820, 'status' => 'C', 'lower' => [7821]]; /* LATIN CAPITAL LETTER X WITH DIAERESIS */ +$config['1e00_1eff'][] = ['upper' => 7822, 'status' => 'C', 'lower' => [7823]]; /* LATIN CAPITAL LETTER Y WITH DOT ABOVE */ +$config['1e00_1eff'][] = ['upper' => 7824, 'status' => 'C', 'lower' => [7825]]; /* LATIN CAPITAL LETTER Z WITH CIRCUMFLEX */ +$config['1e00_1eff'][] = ['upper' => 7826, 'status' => 'C', 'lower' => [7827]]; /* LATIN CAPITAL LETTER Z WITH DOT BELOW */ +$config['1e00_1eff'][] = ['upper' => 7828, 'status' => 'C', 'lower' => [7829]]; /* LATIN CAPITAL LETTER Z WITH LINE BELOW */ //$config['1e00_1eff'][] = array('upper' => 7830, 'status' => 'F', 'lower' => array(104, 817)); /* LATIN SMALL LETTER H WITH LINE BELOW */ //$config['1e00_1eff'][] = array('upper' => 7831, 'status' => 'F', 'lower' => array(116, 776)); /* LATIN SMALL LETTER T WITH DIAERESIS */ @@ -120,48 +120,48 @@ //$config['1e00_1eff'][] = array('upper' => 7834, 'status' => 'F', 'lower' => array(97, 702)); /* LATIN SMALL LETTER A WITH RIGHT HALF RING */ //$config['1e00_1eff'][] = array('upper' => 7835, 'status' => 'C', 'lower' => array(7777)); /* LATIN SMALL LETTER LONG S WITH DOT ABOVE */ -$config['1e00_1eff'][] = array('upper' => 7840, 'status' => 'C', 'lower' => array(7841)); /* LATIN CAPITAL LETTER A WITH DOT BELOW */ -$config['1e00_1eff'][] = array('upper' => 7842, 'status' => 'C', 'lower' => array(7843)); /* LATIN CAPITAL LETTER A WITH HOOK ABOVE */ -$config['1e00_1eff'][] = array('upper' => 7844, 'status' => 'C', 'lower' => array(7845)); /* LATIN CAPITAL LETTER A WITH CIRCUMFLEX AND ACUTE */ -$config['1e00_1eff'][] = array('upper' => 7846, 'status' => 'C', 'lower' => array(7847)); /* LATIN CAPITAL LETTER A WITH CIRCUMFLEX AND GRAVE */ -$config['1e00_1eff'][] = array('upper' => 7848, 'status' => 'C', 'lower' => array(7849)); /* LATIN CAPITAL LETTER A WITH CIRCUMFLEX AND HOOK ABOVE */ -$config['1e00_1eff'][] = array('upper' => 7850, 'status' => 'C', 'lower' => array(7851)); /* LATIN CAPITAL LETTER A WITH CIRCUMFLEX AND TILDE */ -$config['1e00_1eff'][] = array('upper' => 7852, 'status' => 'C', 'lower' => array(7853)); /* LATIN CAPITAL LETTER A WITH CIRCUMFLEX AND DOT BELOW */ -$config['1e00_1eff'][] = array('upper' => 7854, 'status' => 'C', 'lower' => array(7855)); /* LATIN CAPITAL LETTER A WITH BREVE AND ACUTE */ -$config['1e00_1eff'][] = array('upper' => 7856, 'status' => 'C', 'lower' => array(7857)); /* LATIN CAPITAL LETTER A WITH BREVE AND GRAVE */ -$config['1e00_1eff'][] = array('upper' => 7858, 'status' => 'C', 'lower' => array(7859)); /* LATIN CAPITAL LETTER A WITH BREVE AND HOOK ABOVE */ -$config['1e00_1eff'][] = array('upper' => 7860, 'status' => 'C', 'lower' => array(7861)); /* LATIN CAPITAL LETTER A WITH BREVE AND TILDE */ -$config['1e00_1eff'][] = array('upper' => 7862, 'status' => 'C', 'lower' => array(7863)); /* LATIN CAPITAL LETTER A WITH BREVE AND DOT BELOW */ -$config['1e00_1eff'][] = array('upper' => 7864, 'status' => 'C', 'lower' => array(7865)); /* LATIN CAPITAL LETTER E WITH DOT BELOW */ -$config['1e00_1eff'][] = array('upper' => 7866, 'status' => 'C', 'lower' => array(7867)); /* LATIN CAPITAL LETTER E WITH HOOK ABOVE */ -$config['1e00_1eff'][] = array('upper' => 7868, 'status' => 'C', 'lower' => array(7869)); /* LATIN CAPITAL LETTER E WITH TILDE */ -$config['1e00_1eff'][] = array('upper' => 7870, 'status' => 'C', 'lower' => array(7871)); /* LATIN CAPITAL LETTER E WITH CIRCUMFLEX AND ACUTE */ -$config['1e00_1eff'][] = array('upper' => 7872, 'status' => 'C', 'lower' => array(7873)); /* LATIN CAPITAL LETTER E WITH CIRCUMFLEX AND GRAVE */ -$config['1e00_1eff'][] = array('upper' => 7874, 'status' => 'C', 'lower' => array(7875)); /* LATIN CAPITAL LETTER E WITH CIRCUMFLEX AND HOOK ABOVE */ -$config['1e00_1eff'][] = array('upper' => 7876, 'status' => 'C', 'lower' => array(7877)); /* LATIN CAPITAL LETTER E WITH CIRCUMFLEX AND TILDE */ -$config['1e00_1eff'][] = array('upper' => 7878, 'status' => 'C', 'lower' => array(7879)); /* LATIN CAPITAL LETTER E WITH CIRCUMFLEX AND DOT BELOW */ -$config['1e00_1eff'][] = array('upper' => 7880, 'status' => 'C', 'lower' => array(7881)); /* LATIN CAPITAL LETTER I WITH HOOK ABOVE */ -$config['1e00_1eff'][] = array('upper' => 7882, 'status' => 'C', 'lower' => array(7883)); /* LATIN CAPITAL LETTER I WITH DOT BELOW */ -$config['1e00_1eff'][] = array('upper' => 7884, 'status' => 'C', 'lower' => array(7885)); /* LATIN CAPITAL LETTER O WITH DOT BELOW */ -$config['1e00_1eff'][] = array('upper' => 7886, 'status' => 'C', 'lower' => array(7887)); /* LATIN CAPITAL LETTER O WITH HOOK ABOVE */ -$config['1e00_1eff'][] = array('upper' => 7888, 'status' => 'C', 'lower' => array(7889)); /* LATIN CAPITAL LETTER O WITH CIRCUMFLEX AND ACUTE */ -$config['1e00_1eff'][] = array('upper' => 7890, 'status' => 'C', 'lower' => array(7891)); /* LATIN CAPITAL LETTER O WITH CIRCUMFLEX AND GRAVE */ -$config['1e00_1eff'][] = array('upper' => 7892, 'status' => 'C', 'lower' => array(7893)); /* LATIN CAPITAL LETTER O WITH CIRCUMFLEX AND HOOK ABOVE */ -$config['1e00_1eff'][] = array('upper' => 7894, 'status' => 'C', 'lower' => array(7895)); /* LATIN CAPITAL LETTER O WITH CIRCUMFLEX AND TILDE */ -$config['1e00_1eff'][] = array('upper' => 7896, 'status' => 'C', 'lower' => array(7897)); /* LATIN CAPITAL LETTER O WITH CIRCUMFLEX AND DOT BELOW */ -$config['1e00_1eff'][] = array('upper' => 7898, 'status' => 'C', 'lower' => array(7899)); /* LATIN CAPITAL LETTER O WITH HORN AND ACUTE */ -$config['1e00_1eff'][] = array('upper' => 7900, 'status' => 'C', 'lower' => array(7901)); /* LATIN CAPITAL LETTER O WITH HORN AND GRAVE */ -$config['1e00_1eff'][] = array('upper' => 7902, 'status' => 'C', 'lower' => array(7903)); /* LATIN CAPITAL LETTER O WITH HORN AND HOOK ABOVE */ -$config['1e00_1eff'][] = array('upper' => 7904, 'status' => 'C', 'lower' => array(7905)); /* LATIN CAPITAL LETTER O WITH HORN AND TILDE */ -$config['1e00_1eff'][] = array('upper' => 7906, 'status' => 'C', 'lower' => array(7907)); /* LATIN CAPITAL LETTER O WITH HORN AND DOT BELOW */ -$config['1e00_1eff'][] = array('upper' => 7908, 'status' => 'C', 'lower' => array(7909)); /* LATIN CAPITAL LETTER U WITH DOT BELOW */ -$config['1e00_1eff'][] = array('upper' => 7910, 'status' => 'C', 'lower' => array(7911)); /* LATIN CAPITAL LETTER U WITH HOOK ABOVE */ -$config['1e00_1eff'][] = array('upper' => 7912, 'status' => 'C', 'lower' => array(7913)); /* LATIN CAPITAL LETTER U WITH HORN AND ACUTE */ -$config['1e00_1eff'][] = array('upper' => 7914, 'status' => 'C', 'lower' => array(7915)); /* LATIN CAPITAL LETTER U WITH HORN AND GRAVE */ -$config['1e00_1eff'][] = array('upper' => 7916, 'status' => 'C', 'lower' => array(7917)); /* LATIN CAPITAL LETTER U WITH HORN AND HOOK ABOVE */ -$config['1e00_1eff'][] = array('upper' => 7918, 'status' => 'C', 'lower' => array(7919)); /* LATIN CAPITAL LETTER U WITH HORN AND TILDE */ -$config['1e00_1eff'][] = array('upper' => 7920, 'status' => 'C', 'lower' => array(7921)); /* LATIN CAPITAL LETTER U WITH HORN AND DOT BELOW */ -$config['1e00_1eff'][] = array('upper' => 7922, 'status' => 'C', 'lower' => array(7923)); /* LATIN CAPITAL LETTER Y WITH GRAVE */ -$config['1e00_1eff'][] = array('upper' => 7924, 'status' => 'C', 'lower' => array(7925)); /* LATIN CAPITAL LETTER Y WITH DOT BELOW */ -$config['1e00_1eff'][] = array('upper' => 7926, 'status' => 'C', 'lower' => array(7927)); /* LATIN CAPITAL LETTER Y WITH HOOK ABOVE */ -$config['1e00_1eff'][] = array('upper' => 7928, 'status' => 'C', 'lower' => array(7929)); /* LATIN CAPITAL LETTER Y WITH TILDE */ +$config['1e00_1eff'][] = ['upper' => 7840, 'status' => 'C', 'lower' => [7841]]; /* LATIN CAPITAL LETTER A WITH DOT BELOW */ +$config['1e00_1eff'][] = ['upper' => 7842, 'status' => 'C', 'lower' => [7843]]; /* LATIN CAPITAL LETTER A WITH HOOK ABOVE */ +$config['1e00_1eff'][] = ['upper' => 7844, 'status' => 'C', 'lower' => [7845]]; /* LATIN CAPITAL LETTER A WITH CIRCUMFLEX AND ACUTE */ +$config['1e00_1eff'][] = ['upper' => 7846, 'status' => 'C', 'lower' => [7847]]; /* LATIN CAPITAL LETTER A WITH CIRCUMFLEX AND GRAVE */ +$config['1e00_1eff'][] = ['upper' => 7848, 'status' => 'C', 'lower' => [7849]]; /* LATIN CAPITAL LETTER A WITH CIRCUMFLEX AND HOOK ABOVE */ +$config['1e00_1eff'][] = ['upper' => 7850, 'status' => 'C', 'lower' => [7851]]; /* LATIN CAPITAL LETTER A WITH CIRCUMFLEX AND TILDE */ +$config['1e00_1eff'][] = ['upper' => 7852, 'status' => 'C', 'lower' => [7853]]; /* LATIN CAPITAL LETTER A WITH CIRCUMFLEX AND DOT BELOW */ +$config['1e00_1eff'][] = ['upper' => 7854, 'status' => 'C', 'lower' => [7855]]; /* LATIN CAPITAL LETTER A WITH BREVE AND ACUTE */ +$config['1e00_1eff'][] = ['upper' => 7856, 'status' => 'C', 'lower' => [7857]]; /* LATIN CAPITAL LETTER A WITH BREVE AND GRAVE */ +$config['1e00_1eff'][] = ['upper' => 7858, 'status' => 'C', 'lower' => [7859]]; /* LATIN CAPITAL LETTER A WITH BREVE AND HOOK ABOVE */ +$config['1e00_1eff'][] = ['upper' => 7860, 'status' => 'C', 'lower' => [7861]]; /* LATIN CAPITAL LETTER A WITH BREVE AND TILDE */ +$config['1e00_1eff'][] = ['upper' => 7862, 'status' => 'C', 'lower' => [7863]]; /* LATIN CAPITAL LETTER A WITH BREVE AND DOT BELOW */ +$config['1e00_1eff'][] = ['upper' => 7864, 'status' => 'C', 'lower' => [7865]]; /* LATIN CAPITAL LETTER E WITH DOT BELOW */ +$config['1e00_1eff'][] = ['upper' => 7866, 'status' => 'C', 'lower' => [7867]]; /* LATIN CAPITAL LETTER E WITH HOOK ABOVE */ +$config['1e00_1eff'][] = ['upper' => 7868, 'status' => 'C', 'lower' => [7869]]; /* LATIN CAPITAL LETTER E WITH TILDE */ +$config['1e00_1eff'][] = ['upper' => 7870, 'status' => 'C', 'lower' => [7871]]; /* LATIN CAPITAL LETTER E WITH CIRCUMFLEX AND ACUTE */ +$config['1e00_1eff'][] = ['upper' => 7872, 'status' => 'C', 'lower' => [7873]]; /* LATIN CAPITAL LETTER E WITH CIRCUMFLEX AND GRAVE */ +$config['1e00_1eff'][] = ['upper' => 7874, 'status' => 'C', 'lower' => [7875]]; /* LATIN CAPITAL LETTER E WITH CIRCUMFLEX AND HOOK ABOVE */ +$config['1e00_1eff'][] = ['upper' => 7876, 'status' => 'C', 'lower' => [7877]]; /* LATIN CAPITAL LETTER E WITH CIRCUMFLEX AND TILDE */ +$config['1e00_1eff'][] = ['upper' => 7878, 'status' => 'C', 'lower' => [7879]]; /* LATIN CAPITAL LETTER E WITH CIRCUMFLEX AND DOT BELOW */ +$config['1e00_1eff'][] = ['upper' => 7880, 'status' => 'C', 'lower' => [7881]]; /* LATIN CAPITAL LETTER I WITH HOOK ABOVE */ +$config['1e00_1eff'][] = ['upper' => 7882, 'status' => 'C', 'lower' => [7883]]; /* LATIN CAPITAL LETTER I WITH DOT BELOW */ +$config['1e00_1eff'][] = ['upper' => 7884, 'status' => 'C', 'lower' => [7885]]; /* LATIN CAPITAL LETTER O WITH DOT BELOW */ +$config['1e00_1eff'][] = ['upper' => 7886, 'status' => 'C', 'lower' => [7887]]; /* LATIN CAPITAL LETTER O WITH HOOK ABOVE */ +$config['1e00_1eff'][] = ['upper' => 7888, 'status' => 'C', 'lower' => [7889]]; /* LATIN CAPITAL LETTER O WITH CIRCUMFLEX AND ACUTE */ +$config['1e00_1eff'][] = ['upper' => 7890, 'status' => 'C', 'lower' => [7891]]; /* LATIN CAPITAL LETTER O WITH CIRCUMFLEX AND GRAVE */ +$config['1e00_1eff'][] = ['upper' => 7892, 'status' => 'C', 'lower' => [7893]]; /* LATIN CAPITAL LETTER O WITH CIRCUMFLEX AND HOOK ABOVE */ +$config['1e00_1eff'][] = ['upper' => 7894, 'status' => 'C', 'lower' => [7895]]; /* LATIN CAPITAL LETTER O WITH CIRCUMFLEX AND TILDE */ +$config['1e00_1eff'][] = ['upper' => 7896, 'status' => 'C', 'lower' => [7897]]; /* LATIN CAPITAL LETTER O WITH CIRCUMFLEX AND DOT BELOW */ +$config['1e00_1eff'][] = ['upper' => 7898, 'status' => 'C', 'lower' => [7899]]; /* LATIN CAPITAL LETTER O WITH HORN AND ACUTE */ +$config['1e00_1eff'][] = ['upper' => 7900, 'status' => 'C', 'lower' => [7901]]; /* LATIN CAPITAL LETTER O WITH HORN AND GRAVE */ +$config['1e00_1eff'][] = ['upper' => 7902, 'status' => 'C', 'lower' => [7903]]; /* LATIN CAPITAL LETTER O WITH HORN AND HOOK ABOVE */ +$config['1e00_1eff'][] = ['upper' => 7904, 'status' => 'C', 'lower' => [7905]]; /* LATIN CAPITAL LETTER O WITH HORN AND TILDE */ +$config['1e00_1eff'][] = ['upper' => 7906, 'status' => 'C', 'lower' => [7907]]; /* LATIN CAPITAL LETTER O WITH HORN AND DOT BELOW */ +$config['1e00_1eff'][] = ['upper' => 7908, 'status' => 'C', 'lower' => [7909]]; /* LATIN CAPITAL LETTER U WITH DOT BELOW */ +$config['1e00_1eff'][] = ['upper' => 7910, 'status' => 'C', 'lower' => [7911]]; /* LATIN CAPITAL LETTER U WITH HOOK ABOVE */ +$config['1e00_1eff'][] = ['upper' => 7912, 'status' => 'C', 'lower' => [7913]]; /* LATIN CAPITAL LETTER U WITH HORN AND ACUTE */ +$config['1e00_1eff'][] = ['upper' => 7914, 'status' => 'C', 'lower' => [7915]]; /* LATIN CAPITAL LETTER U WITH HORN AND GRAVE */ +$config['1e00_1eff'][] = ['upper' => 7916, 'status' => 'C', 'lower' => [7917]]; /* LATIN CAPITAL LETTER U WITH HORN AND HOOK ABOVE */ +$config['1e00_1eff'][] = ['upper' => 7918, 'status' => 'C', 'lower' => [7919]]; /* LATIN CAPITAL LETTER U WITH HORN AND TILDE */ +$config['1e00_1eff'][] = ['upper' => 7920, 'status' => 'C', 'lower' => [7921]]; /* LATIN CAPITAL LETTER U WITH HORN AND DOT BELOW */ +$config['1e00_1eff'][] = ['upper' => 7922, 'status' => 'C', 'lower' => [7923]]; /* LATIN CAPITAL LETTER Y WITH GRAVE */ +$config['1e00_1eff'][] = ['upper' => 7924, 'status' => 'C', 'lower' => [7925]]; /* LATIN CAPITAL LETTER Y WITH DOT BELOW */ +$config['1e00_1eff'][] = ['upper' => 7926, 'status' => 'C', 'lower' => [7927]]; /* LATIN CAPITAL LETTER Y WITH HOOK ABOVE */ +$config['1e00_1eff'][] = ['upper' => 7928, 'status' => 'C', 'lower' => [7929]]; /* LATIN CAPITAL LETTER Y WITH TILDE */ diff --git a/lib/Cake/Config/unicode/casefolding/1f00_1fff.php b/lib/Cake/Config/unicode/casefolding/1f00_1fff.php index 1b3d6367..00e68d4e 100755 --- a/lib/Cake/Config/unicode/casefolding/1f00_1fff.php +++ b/lib/Cake/Config/unicode/casefolding/1f00_1fff.php @@ -27,7 +27,7 @@ * * The lower filed is an array of the decimal values that form the lower case version of a character. * - * The status field is: + * The status field is: * C: common case folding, common mappings shared by both simple and full mappings. * F: full case folding, mappings that cause strings to grow in length. Multiple characters are separated by spaces. * S: simple case folding, mappings to single characters where different from F. @@ -37,179 +37,179 @@ * Note that the Turkic mappings do not maintain canonical equivalence without additional processing. * See the discussions of case mapping in the Unicode Standard for more information. */ -$config['1f00_1fff'][] = array('upper' => 7944, 'status' => 'C', 'lower' => array(7936, 953)); /* GREEK CAPITAL LETTER ALPHA WITH PSILI */ -$config['1f00_1fff'][] = array('upper' => 7945, 'status' => 'C', 'lower' => array(7937)); /* GREEK CAPITAL LETTER ALPHA WITH DASIA */ -$config['1f00_1fff'][] = array('upper' => 7946, 'status' => 'C', 'lower' => array(7938)); /* GREEK CAPITAL LETTER ALPHA WITH PSILI AND VARIA */ -$config['1f00_1fff'][] = array('upper' => 7947, 'status' => 'C', 'lower' => array(7939)); /* GREEK CAPITAL LETTER ALPHA WITH DASIA AND VARIA */ -$config['1f00_1fff'][] = array('upper' => 7948, 'status' => 'C', 'lower' => array(7940)); /* GREEK CAPITAL LETTER ALPHA WITH PSILI AND OXIA */ -$config['1f00_1fff'][] = array('upper' => 7949, 'status' => 'C', 'lower' => array(7941)); /* GREEK CAPITAL LETTER ALPHA WITH DASIA AND OXIA */ -$config['1f00_1fff'][] = array('upper' => 7950, 'status' => 'C', 'lower' => array(7942)); /* GREEK CAPITAL LETTER ALPHA WITH PSILI AND PERISPOMENI */ -$config['1f00_1fff'][] = array('upper' => 7951, 'status' => 'C', 'lower' => array(7943)); /* GREEK CAPITAL LETTER ALPHA WITH DASIA AND PERISPOMENI */ -$config['1f00_1fff'][] = array('upper' => 7960, 'status' => 'C', 'lower' => array(7952)); /* GREEK CAPITAL LETTER EPSILON WITH PSILI */ -$config['1f00_1fff'][] = array('upper' => 7961, 'status' => 'C', 'lower' => array(7953)); /* GREEK CAPITAL LETTER EPSILON WITH DASIA */ -$config['1f00_1fff'][] = array('upper' => 7962, 'status' => 'C', 'lower' => array(7954)); /* GREEK CAPITAL LETTER EPSILON WITH PSILI AND VARIA */ -$config['1f00_1fff'][] = array('upper' => 7963, 'status' => 'C', 'lower' => array(7955)); /* GREEK CAPITAL LETTER EPSILON WITH DASIA AND VARIA */ -$config['1f00_1fff'][] = array('upper' => 7964, 'status' => 'C', 'lower' => array(7956)); /* GREEK CAPITAL LETTER EPSILON WITH PSILI AND OXIA */ -$config['1f00_1fff'][] = array('upper' => 7965, 'status' => 'C', 'lower' => array(7957)); /* GREEK CAPITAL LETTER EPSILON WITH DASIA AND OXIA */ -$config['1f00_1fff'][] = array('upper' => 7976, 'status' => 'C', 'lower' => array(7968)); /* GREEK CAPITAL LETTER ETA WITH PSILI */ -$config['1f00_1fff'][] = array('upper' => 7977, 'status' => 'C', 'lower' => array(7969)); /* GREEK CAPITAL LETTER ETA WITH DASIA */ -$config['1f00_1fff'][] = array('upper' => 7978, 'status' => 'C', 'lower' => array(7970)); /* GREEK CAPITAL LETTER ETA WITH PSILI AND VARIA */ -$config['1f00_1fff'][] = array('upper' => 7979, 'status' => 'C', 'lower' => array(7971)); /* GREEK CAPITAL LETTER ETA WITH DASIA AND VARIA */ -$config['1f00_1fff'][] = array('upper' => 7980, 'status' => 'C', 'lower' => array(7972)); /* GREEK CAPITAL LETTER ETA WITH PSILI AND OXIA */ -$config['1f00_1fff'][] = array('upper' => 7981, 'status' => 'C', 'lower' => array(7973)); /* GREEK CAPITAL LETTER ETA WITH DASIA AND OXIA */ -$config['1f00_1fff'][] = array('upper' => 7982, 'status' => 'C', 'lower' => array(7974)); /* GREEK CAPITAL LETTER ETA WITH PSILI AND PERISPOMENI */ -$config['1f00_1fff'][] = array('upper' => 7983, 'status' => 'C', 'lower' => array(7975)); /* GREEK CAPITAL LETTER ETA WITH DASIA AND PERISPOMENI */ -$config['1f00_1fff'][] = array('upper' => 7992, 'status' => 'C', 'lower' => array(7984)); /* GREEK CAPITAL LETTER IOTA WITH PSILI */ -$config['1f00_1fff'][] = array('upper' => 7993, 'status' => 'C', 'lower' => array(7985)); /* GREEK CAPITAL LETTER IOTA WITH DASIA */ -$config['1f00_1fff'][] = array('upper' => 7994, 'status' => 'C', 'lower' => array(7986)); /* GREEK CAPITAL LETTER IOTA WITH PSILI AND VARIA */ -$config['1f00_1fff'][] = array('upper' => 7995, 'status' => 'C', 'lower' => array(7987)); /* GREEK CAPITAL LETTER IOTA WITH DASIA AND VARIA */ -$config['1f00_1fff'][] = array('upper' => 7996, 'status' => 'C', 'lower' => array(7988)); /* GREEK CAPITAL LETTER IOTA WITH PSILI AND OXIA */ -$config['1f00_1fff'][] = array('upper' => 7997, 'status' => 'C', 'lower' => array(7989)); /* GREEK CAPITAL LETTER IOTA WITH DASIA AND OXIA */ -$config['1f00_1fff'][] = array('upper' => 7998, 'status' => 'C', 'lower' => array(7990)); /* GREEK CAPITAL LETTER IOTA WITH PSILI AND PERISPOMENI */ -$config['1f00_1fff'][] = array('upper' => 7999, 'status' => 'C', 'lower' => array(7991)); /* GREEK CAPITAL LETTER IOTA WITH DASIA AND PERISPOMENI */ -$config['1f00_1fff'][] = array('upper' => 8008, 'status' => 'C', 'lower' => array(8000)); /* GREEK CAPITAL LETTER OMICRON WITH PSILI */ -$config['1f00_1fff'][] = array('upper' => 8009, 'status' => 'C', 'lower' => array(8001)); /* GREEK CAPITAL LETTER OMICRON WITH DASIA */ -$config['1f00_1fff'][] = array('upper' => 8010, 'status' => 'C', 'lower' => array(8002)); /* GREEK CAPITAL LETTER OMICRON WITH PSILI AND VARIA */ -$config['1f00_1fff'][] = array('upper' => 8011, 'status' => 'C', 'lower' => array(8003)); /* GREEK CAPITAL LETTER OMICRON WITH DASIA AND VARIA */ -$config['1f00_1fff'][] = array('upper' => 8012, 'status' => 'C', 'lower' => array(8004)); /* GREEK CAPITAL LETTER OMICRON WITH PSILI AND OXIA */ -$config['1f00_1fff'][] = array('upper' => 8013, 'status' => 'C', 'lower' => array(8005)); /* GREEK CAPITAL LETTER OMICRON WITH DASIA AND OXIA */ -$config['1f00_1fff'][] = array('upper' => 8016, 'status' => 'F', 'lower' => array(965, 787)); /* GREEK SMALL LETTER UPSILON WITH PSILI */ -$config['1f00_1fff'][] = array('upper' => 8018, 'status' => 'F', 'lower' => array(965, 787, 768)); /* GREEK SMALL LETTER UPSILON WITH PSILI AND VARIA */ -$config['1f00_1fff'][] = array('upper' => 8020, 'status' => 'F', 'lower' => array(965, 787, 769)); /* GREEK SMALL LETTER UPSILON WITH PSILI AND OXIA */ -$config['1f00_1fff'][] = array('upper' => 8022, 'status' => 'F', 'lower' => array(965, 787, 834)); /* GREEK SMALL LETTER UPSILON WITH PSILI AND PERISPOMENI */ -$config['1f00_1fff'][] = array('upper' => 8025, 'status' => 'C', 'lower' => array(8017)); /* GREEK CAPITAL LETTER UPSILON WITH DASIA */ -$config['1f00_1fff'][] = array('upper' => 8027, 'status' => 'C', 'lower' => array(8019)); /* GREEK CAPITAL LETTER UPSILON WITH DASIA AND VARIA */ -$config['1f00_1fff'][] = array('upper' => 8029, 'status' => 'C', 'lower' => array(8021)); /* GREEK CAPITAL LETTER UPSILON WITH DASIA AND OXIA */ -$config['1f00_1fff'][] = array('upper' => 8031, 'status' => 'C', 'lower' => array(8023)); /* GREEK CAPITAL LETTER UPSILON WITH DASIA AND PERISPOMENI */ -$config['1f00_1fff'][] = array('upper' => 8040, 'status' => 'C', 'lower' => array(8032)); /* GREEK CAPITAL LETTER OMEGA WITH PSILI */ -$config['1f00_1fff'][] = array('upper' => 8041, 'status' => 'C', 'lower' => array(8033)); /* GREEK CAPITAL LETTER OMEGA WITH DASIA */ -$config['1f00_1fff'][] = array('upper' => 8042, 'status' => 'C', 'lower' => array(8034)); /* GREEK CAPITAL LETTER OMEGA WITH PSILI AND VARIA */ -$config['1f00_1fff'][] = array('upper' => 8043, 'status' => 'C', 'lower' => array(8035)); /* GREEK CAPITAL LETTER OMEGA WITH DASIA AND VARIA */ -$config['1f00_1fff'][] = array('upper' => 8044, 'status' => 'C', 'lower' => array(8036)); /* GREEK CAPITAL LETTER OMEGA WITH PSILI AND OXIA */ -$config['1f00_1fff'][] = array('upper' => 8045, 'status' => 'C', 'lower' => array(8037)); /* GREEK CAPITAL LETTER OMEGA WITH DASIA AND OXIA */ -$config['1f00_1fff'][] = array('upper' => 8046, 'status' => 'C', 'lower' => array(8038)); /* GREEK CAPITAL LETTER OMEGA WITH PSILI AND PERISPOMENI */ -$config['1f00_1fff'][] = array('upper' => 8047, 'status' => 'C', 'lower' => array(8039)); /* GREEK CAPITAL LETTER OMEGA WITH DASIA AND PERISPOMENI */ -$config['1f00_1fff'][] = array('upper' => 8064, 'status' => 'F', 'lower' => array(7936, 953)); /* GREEK SMALL LETTER ALPHA WITH PSILI AND YPOGEGRAMMENI */ -$config['1f00_1fff'][] = array('upper' => 8065, 'status' => 'F', 'lower' => array(7937, 953)); /* GREEK SMALL LETTER ALPHA WITH DASIA AND YPOGEGRAMMENI */ -$config['1f00_1fff'][] = array('upper' => 8066, 'status' => 'F', 'lower' => array(7938, 953)); /* GREEK SMALL LETTER ALPHA WITH PSILI AND VARIA AND YPOGEGRAMMENI */ -$config['1f00_1fff'][] = array('upper' => 8067, 'status' => 'F', 'lower' => array(7939, 953)); /* GREEK SMALL LETTER ALPHA WITH DASIA AND VARIA AND YPOGEGRAMMENI */ -$config['1f00_1fff'][] = array('upper' => 8068, 'status' => 'F', 'lower' => array(7940, 953)); /* GREEK SMALL LETTER ALPHA WITH PSILI AND OXIA AND YPOGEGRAMMENI */ -$config['1f00_1fff'][] = array('upper' => 8069, 'status' => 'F', 'lower' => array(7941, 953)); /* GREEK SMALL LETTER ALPHA WITH DASIA AND OXIA AND YPOGEGRAMMENI */ -$config['1f00_1fff'][] = array('upper' => 8070, 'status' => 'F', 'lower' => array(7942, 953)); /* GREEK SMALL LETTER ALPHA WITH PSILI AND PERISPOMENI AND YPOGEGRAMMENI */ -$config['1f00_1fff'][] = array('upper' => 8071, 'status' => 'F', 'lower' => array(7943, 953)); /* GREEK SMALL LETTER ALPHA WITH DASIA AND PERISPOMENI AND YPOGEGRAMMENI */ -$config['1f00_1fff'][] = array('upper' => 8072, 'status' => 'F', 'lower' => array(7936, 953)); /* GREEK CAPITAL LETTER ALPHA WITH PSILI AND PROSGEGRAMMENI */ -$config['1f00_1fff'][] = array('upper' => 8072, 'status' => 'S', 'lower' => array(8064)); /* GREEK CAPITAL LETTER ALPHA WITH PSILI AND PROSGEGRAMMENI */ -$config['1f00_1fff'][] = array('upper' => 8073, 'status' => 'F', 'lower' => array(7937, 953)); /* GREEK CAPITAL LETTER ALPHA WITH DASIA AND PROSGEGRAMMENI */ -$config['1f00_1fff'][] = array('upper' => 8073, 'status' => 'S', 'lower' => array(8065)); /* GREEK CAPITAL LETTER ALPHA WITH DASIA AND PROSGEGRAMMENI */ -$config['1f00_1fff'][] = array('upper' => 8074, 'status' => 'F', 'lower' => array(7938, 953)); /* GREEK CAPITAL LETTER ALPHA WITH PSILI AND VARIA AND PROSGEGRAMMENI */ -$config['1f00_1fff'][] = array('upper' => 8074, 'status' => 'S', 'lower' => array(8066)); /* GREEK CAPITAL LETTER ALPHA WITH PSILI AND VARIA AND PROSGEGRAMMENI */ -$config['1f00_1fff'][] = array('upper' => 8075, 'status' => 'F', 'lower' => array(7939, 953)); /* GREEK CAPITAL LETTER ALPHA WITH DASIA AND VARIA AND PROSGEGRAMMENI */ -$config['1f00_1fff'][] = array('upper' => 8075, 'status' => 'S', 'lower' => array(8067)); /* GREEK CAPITAL LETTER ALPHA WITH DASIA AND VARIA AND PROSGEGRAMMENI */ -$config['1f00_1fff'][] = array('upper' => 8076, 'status' => 'F', 'lower' => array(7940, 953)); /* GREEK CAPITAL LETTER ALPHA WITH PSILI AND OXIA AND PROSGEGRAMMENI */ -$config['1f00_1fff'][] = array('upper' => 8076, 'status' => 'S', 'lower' => array(8068)); /* GREEK CAPITAL LETTER ALPHA WITH PSILI AND OXIA AND PROSGEGRAMMENI */ -$config['1f00_1fff'][] = array('upper' => 8077, 'status' => 'F', 'lower' => array(7941, 953)); /* GREEK CAPITAL LETTER ALPHA WITH DASIA AND OXIA AND PROSGEGRAMMENI */ -$config['1f00_1fff'][] = array('upper' => 8077, 'status' => 'S', 'lower' => array(8069)); /* GREEK CAPITAL LETTER ALPHA WITH DASIA AND OXIA AND PROSGEGRAMMENI */ -$config['1f00_1fff'][] = array('upper' => 8078, 'status' => 'F', 'lower' => array(7942, 953)); /* GREEK CAPITAL LETTER ALPHA WITH PSILI AND PERISPOMENI AND PROSGEGRAMMENI */ -$config['1f00_1fff'][] = array('upper' => 8078, 'status' => 'S', 'lower' => array(8070)); /* GREEK CAPITAL LETTER ALPHA WITH PSILI AND PERISPOMENI AND PROSGEGRAMMENI */ -$config['1f00_1fff'][] = array('upper' => 8079, 'status' => 'F', 'lower' => array(7943, 953)); /* GREEK CAPITAL LETTER ALPHA WITH DASIA AND PERISPOMENI AND PROSGEGRAMMENI */ -$config['1f00_1fff'][] = array('upper' => 8079, 'status' => 'S', 'lower' => array(8071)); /* GREEK CAPITAL LETTER ALPHA WITH DASIA AND PERISPOMENI AND PROSGEGRAMMENI */ -$config['1f00_1fff'][] = array('upper' => 8080, 'status' => 'F', 'lower' => array(7968, 953)); /* GREEK SMALL LETTER ETA WITH PSILI AND YPOGEGRAMMENI */ -$config['1f00_1fff'][] = array('upper' => 8081, 'status' => 'F', 'lower' => array(7969, 953)); /* GREEK SMALL LETTER ETA WITH DASIA AND YPOGEGRAMMENI */ -$config['1f00_1fff'][] = array('upper' => 8082, 'status' => 'F', 'lower' => array(7970, 953)); /* GREEK SMALL LETTER ETA WITH PSILI AND VARIA AND YPOGEGRAMMENI */ -$config['1f00_1fff'][] = array('upper' => 8083, 'status' => 'F', 'lower' => array(7971, 953)); /* GREEK SMALL LETTER ETA WITH DASIA AND VARIA AND YPOGEGRAMMENI */ -$config['1f00_1fff'][] = array('upper' => 8084, 'status' => 'F', 'lower' => array(7972, 953)); /* GREEK SMALL LETTER ETA WITH PSILI AND OXIA AND YPOGEGRAMMENI */ -$config['1f00_1fff'][] = array('upper' => 8085, 'status' => 'F', 'lower' => array(7973, 953)); /* GREEK SMALL LETTER ETA WITH DASIA AND OXIA AND YPOGEGRAMMENI */ -$config['1f00_1fff'][] = array('upper' => 8086, 'status' => 'F', 'lower' => array(7974, 953)); /* GREEK SMALL LETTER ETA WITH PSILI AND PERISPOMENI AND YPOGEGRAMMENI */ -$config['1f00_1fff'][] = array('upper' => 8087, 'status' => 'F', 'lower' => array(7975, 953)); /* GREEK SMALL LETTER ETA WITH DASIA AND PERISPOMENI AND YPOGEGRAMMENI */ -$config['1f00_1fff'][] = array('upper' => 8088, 'status' => 'F', 'lower' => array(7968, 953)); /* GREEK CAPITAL LETTER ETA WITH PSILI AND PROSGEGRAMMENI */ -$config['1f00_1fff'][] = array('upper' => 8088, 'status' => 'S', 'lower' => array(8080)); /* GREEK CAPITAL LETTER ETA WITH PSILI AND PROSGEGRAMMENI */ -$config['1f00_1fff'][] = array('upper' => 8089, 'status' => 'F', 'lower' => array(7969, 953)); /* GREEK CAPITAL LETTER ETA WITH DASIA AND PROSGEGRAMMENI */ -$config['1f00_1fff'][] = array('upper' => 8089, 'status' => 'S', 'lower' => array(8081)); /* GREEK CAPITAL LETTER ETA WITH DASIA AND PROSGEGRAMMENI */ -$config['1f00_1fff'][] = array('upper' => 8090, 'status' => 'F', 'lower' => array(7970, 953)); /* GREEK CAPITAL LETTER ETA WITH PSILI AND VARIA AND PROSGEGRAMMENI */ -$config['1f00_1fff'][] = array('upper' => 8090, 'status' => 'S', 'lower' => array(8082)); /* GREEK CAPITAL LETTER ETA WITH PSILI AND VARIA AND PROSGEGRAMMENI */ -$config['1f00_1fff'][] = array('upper' => 8091, 'status' => 'F', 'lower' => array(7971, 953)); /* GREEK CAPITAL LETTER ETA WITH DASIA AND VARIA AND PROSGEGRAMMENI */ -$config['1f00_1fff'][] = array('upper' => 8091, 'status' => 'S', 'lower' => array(8083)); /* GREEK CAPITAL LETTER ETA WITH DASIA AND VARIA AND PROSGEGRAMMENI */ -$config['1f00_1fff'][] = array('upper' => 8092, 'status' => 'F', 'lower' => array(7972, 953)); /* GREEK CAPITAL LETTER ETA WITH PSILI AND OXIA AND PROSGEGRAMMENI */ -$config['1f00_1fff'][] = array('upper' => 8092, 'status' => 'S', 'lower' => array(8084)); /* GREEK CAPITAL LETTER ETA WITH PSILI AND OXIA AND PROSGEGRAMMENI */ -$config['1f00_1fff'][] = array('upper' => 8093, 'status' => 'F', 'lower' => array(7973, 953)); /* GREEK CAPITAL LETTER ETA WITH DASIA AND OXIA AND PROSGEGRAMMENI */ -$config['1f00_1fff'][] = array('upper' => 8093, 'status' => 'S', 'lower' => array(8085)); /* GREEK CAPITAL LETTER ETA WITH DASIA AND OXIA AND PROSGEGRAMMENI */ -$config['1f00_1fff'][] = array('upper' => 8094, 'status' => 'F', 'lower' => array(7974, 953)); /* GREEK CAPITAL LETTER ETA WITH PSILI AND PERISPOMENI AND PROSGEGRAMMENI */ -$config['1f00_1fff'][] = array('upper' => 8094, 'status' => 'S', 'lower' => array(8086)); /* GREEK CAPITAL LETTER ETA WITH PSILI AND PERISPOMENI AND PROSGEGRAMMENI */ -$config['1f00_1fff'][] = array('upper' => 8095, 'status' => 'F', 'lower' => array(7975, 953)); /* GREEK CAPITAL LETTER ETA WITH DASIA AND PERISPOMENI AND PROSGEGRAMMENI */ -$config['1f00_1fff'][] = array('upper' => 8095, 'status' => 'S', 'lower' => array(8087)); /* GREEK CAPITAL LETTER ETA WITH DASIA AND PERISPOMENI AND PROSGEGRAMMENI */ -$config['1f00_1fff'][] = array('upper' => 8096, 'status' => 'F', 'lower' => array(8032, 953)); /* GREEK SMALL LETTER OMEGA WITH PSILI AND YPOGEGRAMMENI */ -$config['1f00_1fff'][] = array('upper' => 8097, 'status' => 'F', 'lower' => array(8033, 953)); /* GREEK SMALL LETTER OMEGA WITH DASIA AND YPOGEGRAMMENI */ -$config['1f00_1fff'][] = array('upper' => 8098, 'status' => 'F', 'lower' => array(8034, 953)); /* GREEK SMALL LETTER OMEGA WITH PSILI AND VARIA AND YPOGEGRAMMENI */ -$config['1f00_1fff'][] = array('upper' => 8099, 'status' => 'F', 'lower' => array(8035, 953)); /* GREEK SMALL LETTER OMEGA WITH DASIA AND VARIA AND YPOGEGRAMMENI */ -$config['1f00_1fff'][] = array('upper' => 8100, 'status' => 'F', 'lower' => array(8036, 953)); /* GREEK SMALL LETTER OMEGA WITH PSILI AND OXIA AND YPOGEGRAMMENI */ -$config['1f00_1fff'][] = array('upper' => 8101, 'status' => 'F', 'lower' => array(8037, 953)); /* GREEK SMALL LETTER OMEGA WITH DASIA AND OXIA AND YPOGEGRAMMENI */ -$config['1f00_1fff'][] = array('upper' => 8102, 'status' => 'F', 'lower' => array(8038, 953)); /* GREEK SMALL LETTER OMEGA WITH PSILI AND PERISPOMENI AND YPOGEGRAMMENI */ -$config['1f00_1fff'][] = array('upper' => 8103, 'status' => 'F', 'lower' => array(8039, 953)); /* GREEK SMALL LETTER OMEGA WITH DASIA AND PERISPOMENI AND YPOGEGRAMMENI */ -$config['1f00_1fff'][] = array('upper' => 8104, 'status' => 'F', 'lower' => array(8032, 953)); /* GREEK CAPITAL LETTER OMEGA WITH PSILI AND PROSGEGRAMMENI */ -$config['1f00_1fff'][] = array('upper' => 8104, 'status' => 'S', 'lower' => array(8096)); /* GREEK CAPITAL LETTER OMEGA WITH PSILI AND PROSGEGRAMMENI */ -$config['1f00_1fff'][] = array('upper' => 8105, 'status' => 'F', 'lower' => array(8033, 953)); /* GREEK CAPITAL LETTER OMEGA WITH DASIA AND PROSGEGRAMMENI */ -$config['1f00_1fff'][] = array('upper' => 8105, 'status' => 'S', 'lower' => array(8097)); /* GREEK CAPITAL LETTER OMEGA WITH DASIA AND PROSGEGRAMMENI */ -$config['1f00_1fff'][] = array('upper' => 8106, 'status' => 'F', 'lower' => array(8034, 953)); /* GREEK CAPITAL LETTER OMEGA WITH PSILI AND VARIA AND PROSGEGRAMMENI */ -$config['1f00_1fff'][] = array('upper' => 8106, 'status' => 'S', 'lower' => array(8098)); /* GREEK CAPITAL LETTER OMEGA WITH PSILI AND VARIA AND PROSGEGRAMMENI */ -$config['1f00_1fff'][] = array('upper' => 8107, 'status' => 'F', 'lower' => array(8035, 953)); /* GREEK CAPITAL LETTER OMEGA WITH DASIA AND VARIA AND PROSGEGRAMMENI */ -$config['1f00_1fff'][] = array('upper' => 8107, 'status' => 'S', 'lower' => array(8099)); /* GREEK CAPITAL LETTER OMEGA WITH DASIA AND VARIA AND PROSGEGRAMMENI */ -$config['1f00_1fff'][] = array('upper' => 8108, 'status' => 'F', 'lower' => array(8036, 953)); /* GREEK CAPITAL LETTER OMEGA WITH PSILI AND OXIA AND PROSGEGRAMMENI */ -$config['1f00_1fff'][] = array('upper' => 8108, 'status' => 'S', 'lower' => array(8100)); /* GREEK CAPITAL LETTER OMEGA WITH PSILI AND OXIA AND PROSGEGRAMMENI */ -$config['1f00_1fff'][] = array('upper' => 8109, 'status' => 'F', 'lower' => array(8037, 953)); /* GREEK CAPITAL LETTER OMEGA WITH DASIA AND OXIA AND PROSGEGRAMMENI */ -$config['1f00_1fff'][] = array('upper' => 8109, 'status' => 'S', 'lower' => array(8101)); /* GREEK CAPITAL LETTER OMEGA WITH DASIA AND OXIA AND PROSGEGRAMMENI */ -$config['1f00_1fff'][] = array('upper' => 8110, 'status' => 'F', 'lower' => array(8038, 953)); /* GREEK CAPITAL LETTER OMEGA WITH PSILI AND PERISPOMENI AND PROSGEGRAMMENI */ -$config['1f00_1fff'][] = array('upper' => 8110, 'status' => 'S', 'lower' => array(8102)); /* GREEK CAPITAL LETTER OMEGA WITH PSILI AND PERISPOMENI AND PROSGEGRAMMENI */ -$config['1f00_1fff'][] = array('upper' => 8111, 'status' => 'F', 'lower' => array(8039, 953)); /* GREEK CAPITAL LETTER OMEGA WITH DASIA AND PERISPOMENI AND PROSGEGRAMMENI */ -$config['1f00_1fff'][] = array('upper' => 8111, 'status' => 'S', 'lower' => array(8103)); /* GREEK CAPITAL LETTER OMEGA WITH DASIA AND PERISPOMENI AND PROSGEGRAMMENI */ -$config['1f00_1fff'][] = array('upper' => 8114, 'status' => 'F', 'lower' => array(8048, 953)); /* GREEK SMALL LETTER ALPHA WITH VARIA AND YPOGEGRAMMENI */ -$config['1f00_1fff'][] = array('upper' => 8115, 'status' => 'F', 'lower' => array(945, 953)); /* GREEK SMALL LETTER ALPHA WITH YPOGEGRAMMENI */ -$config['1f00_1fff'][] = array('upper' => 8116, 'status' => 'F', 'lower' => array(940, 953)); /* GREEK SMALL LETTER ALPHA WITH OXIA AND YPOGEGRAMMENI */ -$config['1f00_1fff'][] = array('upper' => 8118, 'status' => 'F', 'lower' => array(945, 834)); /* GREEK SMALL LETTER ALPHA WITH PERISPOMENI */ -$config['1f00_1fff'][] = array('upper' => 8119, 'status' => 'F', 'lower' => array(945, 834, 953)); /* GREEK SMALL LETTER ALPHA WITH PERISPOMENI AND YPOGEGRAMMENI */ -$config['1f00_1fff'][] = array('upper' => 8120, 'status' => 'C', 'lower' => array(8112)); /* GREEK CAPITAL LETTER ALPHA WITH VRACHY */ -$config['1f00_1fff'][] = array('upper' => 8121, 'status' => 'C', 'lower' => array(8113)); /* GREEK CAPITAL LETTER ALPHA WITH MACRON */ -$config['1f00_1fff'][] = array('upper' => 8122, 'status' => 'C', 'lower' => array(8048)); /* GREEK CAPITAL LETTER ALPHA WITH VARIA */ -$config['1f00_1fff'][] = array('upper' => 8123, 'status' => 'C', 'lower' => array(8049)); /* GREEK CAPITAL LETTER ALPHA WITH OXIA */ -$config['1f00_1fff'][] = array('upper' => 8124, 'status' => 'F', 'lower' => array(945, 953)); /* GREEK CAPITAL LETTER ALPHA WITH PROSGEGRAMMENI */ -$config['1f00_1fff'][] = array('upper' => 8124, 'status' => 'S', 'lower' => array(8115)); /* GREEK CAPITAL LETTER ALPHA WITH PROSGEGRAMMENI */ -$config['1f00_1fff'][] = array('upper' => 8126, 'status' => 'C', 'lower' => array(953)); /* GREEK PROSGEGRAMMENI */ -$config['1f00_1fff'][] = array('upper' => 8130, 'status' => 'F', 'lower' => array(8052, 953)); /* GREEK SMALL LETTER ETA WITH VARIA AND YPOGEGRAMMENI */ -$config['1f00_1fff'][] = array('upper' => 8131, 'status' => 'F', 'lower' => array(951, 953)); /* GREEK SMALL LETTER ETA WITH YPOGEGRAMMENI */ -$config['1f00_1fff'][] = array('upper' => 8132, 'status' => 'F', 'lower' => array(942, 953)); /* GREEK SMALL LETTER ETA WITH OXIA AND YPOGEGRAMMENI */ -$config['1f00_1fff'][] = array('upper' => 8134, 'status' => 'F', 'lower' => array(951, 834)); /* GREEK SMALL LETTER ETA WITH PERISPOMENI */ -$config['1f00_1fff'][] = array('upper' => 8135, 'status' => 'F', 'lower' => array(951, 834, 953)); /* GREEK SMALL LETTER ETA WITH PERISPOMENI AND YPOGEGRAMMENI */ -$config['1f00_1fff'][] = array('upper' => 8136, 'status' => 'C', 'lower' => array(8050)); /* GREEK CAPITAL LETTER EPSILON WITH VARIA */ -$config['1f00_1fff'][] = array('upper' => 8137, 'status' => 'C', 'lower' => array(8051)); /* GREEK CAPITAL LETTER EPSILON WITH OXIA */ -$config['1f00_1fff'][] = array('upper' => 8138, 'status' => 'C', 'lower' => array(8052)); /* GREEK CAPITAL LETTER ETA WITH VARIA */ -$config['1f00_1fff'][] = array('upper' => 8139, 'status' => 'C', 'lower' => array(8053)); /* GREEK CAPITAL LETTER ETA WITH OXIA */ -$config['1f00_1fff'][] = array('upper' => 8140, 'status' => 'F', 'lower' => array(951, 953)); /* GREEK CAPITAL LETTER ETA WITH PROSGEGRAMMENI */ -$config['1f00_1fff'][] = array('upper' => 8140, 'status' => 'S', 'lower' => array(8131)); /* GREEK CAPITAL LETTER ETA WITH PROSGEGRAMMENI */ -$config['1f00_1fff'][] = array('upper' => 8146, 'status' => 'F', 'lower' => array(953, 776, 768)); /* GREEK SMALL LETTER IOTA WITH DIALYTIKA AND VARIA */ -$config['1f00_1fff'][] = array('upper' => 8147, 'status' => 'F', 'lower' => array(953, 776, 769)); /* GREEK SMALL LETTER IOTA WITH DIALYTIKA AND OXIA */ -$config['1f00_1fff'][] = array('upper' => 8150, 'status' => 'F', 'lower' => array(953, 834)); /* GREEK SMALL LETTER IOTA WITH PERISPOMENI */ -$config['1f00_1fff'][] = array('upper' => 8151, 'status' => 'F', 'lower' => array(953, 776, 834)); /* GREEK SMALL LETTER IOTA WITH DIALYTIKA AND PERISPOMENI */ -$config['1f00_1fff'][] = array('upper' => 8152, 'status' => 'C', 'lower' => array(8144)); /* GREEK CAPITAL LETTER IOTA WITH VRACHY */ -$config['1f00_1fff'][] = array('upper' => 8153, 'status' => 'C', 'lower' => array(8145)); /* GREEK CAPITAL LETTER IOTA WITH MACRON */ -$config['1f00_1fff'][] = array('upper' => 8154, 'status' => 'C', 'lower' => array(8054)); /* GREEK CAPITAL LETTER IOTA WITH VARIA */ -$config['1f00_1fff'][] = array('upper' => 8155, 'status' => 'C', 'lower' => array(8055)); /* GREEK CAPITAL LETTER IOTA WITH OXIA */ -$config['1f00_1fff'][] = array('upper' => 8162, 'status' => 'F', 'lower' => array(965, 776, 768)); /* GREEK SMALL LETTER UPSILON WITH DIALYTIKA AND VARIA */ -$config['1f00_1fff'][] = array('upper' => 8163, 'status' => 'F', 'lower' => array(965, 776, 769)); /* GREEK SMALL LETTER UPSILON WITH DIALYTIKA AND OXIA */ -$config['1f00_1fff'][] = array('upper' => 8164, 'status' => 'F', 'lower' => array(961, 787)); /* GREEK SMALL LETTER RHO WITH PSILI */ -$config['1f00_1fff'][] = array('upper' => 8166, 'status' => 'F', 'lower' => array(965, 834)); /* GREEK SMALL LETTER UPSILON WITH PERISPOMENI */ -$config['1f00_1fff'][] = array('upper' => 8167, 'status' => 'F', 'lower' => array(965, 776, 834)); /* GREEK SMALL LETTER UPSILON WITH DIALYTIKA AND PERISPOMENI */ -$config['1f00_1fff'][] = array('upper' => 8168, 'status' => 'C', 'lower' => array(8160)); /* GREEK CAPITAL LETTER UPSILON WITH VRACHY */ -$config['1f00_1fff'][] = array('upper' => 8169, 'status' => 'C', 'lower' => array(8161)); /* GREEK CAPITAL LETTER UPSILON WITH MACRON */ -$config['1f00_1fff'][] = array('upper' => 8170, 'status' => 'C', 'lower' => array(8058)); /* GREEK CAPITAL LETTER UPSILON WITH VARIA */ -$config['1f00_1fff'][] = array('upper' => 8171, 'status' => 'C', 'lower' => array(8059)); /* GREEK CAPITAL LETTER UPSILON WITH OXIA */ -$config['1f00_1fff'][] = array('upper' => 8172, 'status' => 'C', 'lower' => array(8165)); /* GREEK CAPITAL LETTER RHO WITH DASIA */ -$config['1f00_1fff'][] = array('upper' => 8178, 'status' => 'F', 'lower' => array(8060, 953)); /* GREEK SMALL LETTER OMEGA WITH VARIA AND YPOGEGRAMMENI */ -$config['1f00_1fff'][] = array('upper' => 8179, 'status' => 'F', 'lower' => array(969, 953)); /* GREEK SMALL LETTER OMEGA WITH YPOGEGRAMMENI */ -$config['1f00_1fff'][] = array('upper' => 8180, 'status' => 'F', 'lower' => array(974, 953)); /* GREEK SMALL LETTER OMEGA WITH OXIA AND YPOGEGRAMMENI */ -$config['1f00_1fff'][] = array('upper' => 8182, 'status' => 'F', 'lower' => array(969, 834)); /* GREEK SMALL LETTER OMEGA WITH PERISPOMENI */ -$config['1f00_1fff'][] = array('upper' => 8183, 'status' => 'F', 'lower' => array(969, 834, 953)); /* GREEK SMALL LETTER OMEGA WITH PERISPOMENI AND YPOGEGRAMMENI */ -$config['1f00_1fff'][] = array('upper' => 8184, 'status' => 'C', 'lower' => array(8056)); /* GREEK CAPITAL LETTER OMICRON WITH VARIA */ -$config['1f00_1fff'][] = array('upper' => 8185, 'status' => 'C', 'lower' => array(8057)); /* GREEK CAPITAL LETTER OMICRON WITH OXIA */ -$config['1f00_1fff'][] = array('upper' => 8186, 'status' => 'C', 'lower' => array(8060)); /* GREEK CAPITAL LETTER OMEGA WITH VARIA */ -$config['1f00_1fff'][] = array('upper' => 8187, 'status' => 'C', 'lower' => array(8061)); /* GREEK CAPITAL LETTER OMEGA WITH OXIA */ -$config['1f00_1fff'][] = array('upper' => 8188, 'status' => 'F', 'lower' => array(969, 953)); /* GREEK CAPITAL LETTER OMEGA WITH PROSGEGRAMMENI */ -$config['1f00_1fff'][] = array('upper' => 8188, 'status' => 'S', 'lower' => array(8179)); /* GREEK CAPITAL LETTER OMEGA WITH PROSGEGRAMMENI */ +$config['1f00_1fff'][] = ['upper' => 7944, 'status' => 'C', 'lower' => [7936, 953]]; /* GREEK CAPITAL LETTER ALPHA WITH PSILI */ +$config['1f00_1fff'][] = ['upper' => 7945, 'status' => 'C', 'lower' => [7937]]; /* GREEK CAPITAL LETTER ALPHA WITH DASIA */ +$config['1f00_1fff'][] = ['upper' => 7946, 'status' => 'C', 'lower' => [7938]]; /* GREEK CAPITAL LETTER ALPHA WITH PSILI AND VARIA */ +$config['1f00_1fff'][] = ['upper' => 7947, 'status' => 'C', 'lower' => [7939]]; /* GREEK CAPITAL LETTER ALPHA WITH DASIA AND VARIA */ +$config['1f00_1fff'][] = ['upper' => 7948, 'status' => 'C', 'lower' => [7940]]; /* GREEK CAPITAL LETTER ALPHA WITH PSILI AND OXIA */ +$config['1f00_1fff'][] = ['upper' => 7949, 'status' => 'C', 'lower' => [7941]]; /* GREEK CAPITAL LETTER ALPHA WITH DASIA AND OXIA */ +$config['1f00_1fff'][] = ['upper' => 7950, 'status' => 'C', 'lower' => [7942]]; /* GREEK CAPITAL LETTER ALPHA WITH PSILI AND PERISPOMENI */ +$config['1f00_1fff'][] = ['upper' => 7951, 'status' => 'C', 'lower' => [7943]]; /* GREEK CAPITAL LETTER ALPHA WITH DASIA AND PERISPOMENI */ +$config['1f00_1fff'][] = ['upper' => 7960, 'status' => 'C', 'lower' => [7952]]; /* GREEK CAPITAL LETTER EPSILON WITH PSILI */ +$config['1f00_1fff'][] = ['upper' => 7961, 'status' => 'C', 'lower' => [7953]]; /* GREEK CAPITAL LETTER EPSILON WITH DASIA */ +$config['1f00_1fff'][] = ['upper' => 7962, 'status' => 'C', 'lower' => [7954]]; /* GREEK CAPITAL LETTER EPSILON WITH PSILI AND VARIA */ +$config['1f00_1fff'][] = ['upper' => 7963, 'status' => 'C', 'lower' => [7955]]; /* GREEK CAPITAL LETTER EPSILON WITH DASIA AND VARIA */ +$config['1f00_1fff'][] = ['upper' => 7964, 'status' => 'C', 'lower' => [7956]]; /* GREEK CAPITAL LETTER EPSILON WITH PSILI AND OXIA */ +$config['1f00_1fff'][] = ['upper' => 7965, 'status' => 'C', 'lower' => [7957]]; /* GREEK CAPITAL LETTER EPSILON WITH DASIA AND OXIA */ +$config['1f00_1fff'][] = ['upper' => 7976, 'status' => 'C', 'lower' => [7968]]; /* GREEK CAPITAL LETTER ETA WITH PSILI */ +$config['1f00_1fff'][] = ['upper' => 7977, 'status' => 'C', 'lower' => [7969]]; /* GREEK CAPITAL LETTER ETA WITH DASIA */ +$config['1f00_1fff'][] = ['upper' => 7978, 'status' => 'C', 'lower' => [7970]]; /* GREEK CAPITAL LETTER ETA WITH PSILI AND VARIA */ +$config['1f00_1fff'][] = ['upper' => 7979, 'status' => 'C', 'lower' => [7971]]; /* GREEK CAPITAL LETTER ETA WITH DASIA AND VARIA */ +$config['1f00_1fff'][] = ['upper' => 7980, 'status' => 'C', 'lower' => [7972]]; /* GREEK CAPITAL LETTER ETA WITH PSILI AND OXIA */ +$config['1f00_1fff'][] = ['upper' => 7981, 'status' => 'C', 'lower' => [7973]]; /* GREEK CAPITAL LETTER ETA WITH DASIA AND OXIA */ +$config['1f00_1fff'][] = ['upper' => 7982, 'status' => 'C', 'lower' => [7974]]; /* GREEK CAPITAL LETTER ETA WITH PSILI AND PERISPOMENI */ +$config['1f00_1fff'][] = ['upper' => 7983, 'status' => 'C', 'lower' => [7975]]; /* GREEK CAPITAL LETTER ETA WITH DASIA AND PERISPOMENI */ +$config['1f00_1fff'][] = ['upper' => 7992, 'status' => 'C', 'lower' => [7984]]; /* GREEK CAPITAL LETTER IOTA WITH PSILI */ +$config['1f00_1fff'][] = ['upper' => 7993, 'status' => 'C', 'lower' => [7985]]; /* GREEK CAPITAL LETTER IOTA WITH DASIA */ +$config['1f00_1fff'][] = ['upper' => 7994, 'status' => 'C', 'lower' => [7986]]; /* GREEK CAPITAL LETTER IOTA WITH PSILI AND VARIA */ +$config['1f00_1fff'][] = ['upper' => 7995, 'status' => 'C', 'lower' => [7987]]; /* GREEK CAPITAL LETTER IOTA WITH DASIA AND VARIA */ +$config['1f00_1fff'][] = ['upper' => 7996, 'status' => 'C', 'lower' => [7988]]; /* GREEK CAPITAL LETTER IOTA WITH PSILI AND OXIA */ +$config['1f00_1fff'][] = ['upper' => 7997, 'status' => 'C', 'lower' => [7989]]; /* GREEK CAPITAL LETTER IOTA WITH DASIA AND OXIA */ +$config['1f00_1fff'][] = ['upper' => 7998, 'status' => 'C', 'lower' => [7990]]; /* GREEK CAPITAL LETTER IOTA WITH PSILI AND PERISPOMENI */ +$config['1f00_1fff'][] = ['upper' => 7999, 'status' => 'C', 'lower' => [7991]]; /* GREEK CAPITAL LETTER IOTA WITH DASIA AND PERISPOMENI */ +$config['1f00_1fff'][] = ['upper' => 8008, 'status' => 'C', 'lower' => [8000]]; /* GREEK CAPITAL LETTER OMICRON WITH PSILI */ +$config['1f00_1fff'][] = ['upper' => 8009, 'status' => 'C', 'lower' => [8001]]; /* GREEK CAPITAL LETTER OMICRON WITH DASIA */ +$config['1f00_1fff'][] = ['upper' => 8010, 'status' => 'C', 'lower' => [8002]]; /* GREEK CAPITAL LETTER OMICRON WITH PSILI AND VARIA */ +$config['1f00_1fff'][] = ['upper' => 8011, 'status' => 'C', 'lower' => [8003]]; /* GREEK CAPITAL LETTER OMICRON WITH DASIA AND VARIA */ +$config['1f00_1fff'][] = ['upper' => 8012, 'status' => 'C', 'lower' => [8004]]; /* GREEK CAPITAL LETTER OMICRON WITH PSILI AND OXIA */ +$config['1f00_1fff'][] = ['upper' => 8013, 'status' => 'C', 'lower' => [8005]]; /* GREEK CAPITAL LETTER OMICRON WITH DASIA AND OXIA */ +$config['1f00_1fff'][] = ['upper' => 8016, 'status' => 'F', 'lower' => [965, 787]]; /* GREEK SMALL LETTER UPSILON WITH PSILI */ +$config['1f00_1fff'][] = ['upper' => 8018, 'status' => 'F', 'lower' => [965, 787, 768]]; /* GREEK SMALL LETTER UPSILON WITH PSILI AND VARIA */ +$config['1f00_1fff'][] = ['upper' => 8020, 'status' => 'F', 'lower' => [965, 787, 769]]; /* GREEK SMALL LETTER UPSILON WITH PSILI AND OXIA */ +$config['1f00_1fff'][] = ['upper' => 8022, 'status' => 'F', 'lower' => [965, 787, 834]]; /* GREEK SMALL LETTER UPSILON WITH PSILI AND PERISPOMENI */ +$config['1f00_1fff'][] = ['upper' => 8025, 'status' => 'C', 'lower' => [8017]]; /* GREEK CAPITAL LETTER UPSILON WITH DASIA */ +$config['1f00_1fff'][] = ['upper' => 8027, 'status' => 'C', 'lower' => [8019]]; /* GREEK CAPITAL LETTER UPSILON WITH DASIA AND VARIA */ +$config['1f00_1fff'][] = ['upper' => 8029, 'status' => 'C', 'lower' => [8021]]; /* GREEK CAPITAL LETTER UPSILON WITH DASIA AND OXIA */ +$config['1f00_1fff'][] = ['upper' => 8031, 'status' => 'C', 'lower' => [8023]]; /* GREEK CAPITAL LETTER UPSILON WITH DASIA AND PERISPOMENI */ +$config['1f00_1fff'][] = ['upper' => 8040, 'status' => 'C', 'lower' => [8032]]; /* GREEK CAPITAL LETTER OMEGA WITH PSILI */ +$config['1f00_1fff'][] = ['upper' => 8041, 'status' => 'C', 'lower' => [8033]]; /* GREEK CAPITAL LETTER OMEGA WITH DASIA */ +$config['1f00_1fff'][] = ['upper' => 8042, 'status' => 'C', 'lower' => [8034]]; /* GREEK CAPITAL LETTER OMEGA WITH PSILI AND VARIA */ +$config['1f00_1fff'][] = ['upper' => 8043, 'status' => 'C', 'lower' => [8035]]; /* GREEK CAPITAL LETTER OMEGA WITH DASIA AND VARIA */ +$config['1f00_1fff'][] = ['upper' => 8044, 'status' => 'C', 'lower' => [8036]]; /* GREEK CAPITAL LETTER OMEGA WITH PSILI AND OXIA */ +$config['1f00_1fff'][] = ['upper' => 8045, 'status' => 'C', 'lower' => [8037]]; /* GREEK CAPITAL LETTER OMEGA WITH DASIA AND OXIA */ +$config['1f00_1fff'][] = ['upper' => 8046, 'status' => 'C', 'lower' => [8038]]; /* GREEK CAPITAL LETTER OMEGA WITH PSILI AND PERISPOMENI */ +$config['1f00_1fff'][] = ['upper' => 8047, 'status' => 'C', 'lower' => [8039]]; /* GREEK CAPITAL LETTER OMEGA WITH DASIA AND PERISPOMENI */ +$config['1f00_1fff'][] = ['upper' => 8064, 'status' => 'F', 'lower' => [7936, 953]]; /* GREEK SMALL LETTER ALPHA WITH PSILI AND YPOGEGRAMMENI */ +$config['1f00_1fff'][] = ['upper' => 8065, 'status' => 'F', 'lower' => [7937, 953]]; /* GREEK SMALL LETTER ALPHA WITH DASIA AND YPOGEGRAMMENI */ +$config['1f00_1fff'][] = ['upper' => 8066, 'status' => 'F', 'lower' => [7938, 953]]; /* GREEK SMALL LETTER ALPHA WITH PSILI AND VARIA AND YPOGEGRAMMENI */ +$config['1f00_1fff'][] = ['upper' => 8067, 'status' => 'F', 'lower' => [7939, 953]]; /* GREEK SMALL LETTER ALPHA WITH DASIA AND VARIA AND YPOGEGRAMMENI */ +$config['1f00_1fff'][] = ['upper' => 8068, 'status' => 'F', 'lower' => [7940, 953]]; /* GREEK SMALL LETTER ALPHA WITH PSILI AND OXIA AND YPOGEGRAMMENI */ +$config['1f00_1fff'][] = ['upper' => 8069, 'status' => 'F', 'lower' => [7941, 953]]; /* GREEK SMALL LETTER ALPHA WITH DASIA AND OXIA AND YPOGEGRAMMENI */ +$config['1f00_1fff'][] = ['upper' => 8070, 'status' => 'F', 'lower' => [7942, 953]]; /* GREEK SMALL LETTER ALPHA WITH PSILI AND PERISPOMENI AND YPOGEGRAMMENI */ +$config['1f00_1fff'][] = ['upper' => 8071, 'status' => 'F', 'lower' => [7943, 953]]; /* GREEK SMALL LETTER ALPHA WITH DASIA AND PERISPOMENI AND YPOGEGRAMMENI */ +$config['1f00_1fff'][] = ['upper' => 8072, 'status' => 'F', 'lower' => [7936, 953]]; /* GREEK CAPITAL LETTER ALPHA WITH PSILI AND PROSGEGRAMMENI */ +$config['1f00_1fff'][] = ['upper' => 8072, 'status' => 'S', 'lower' => [8064]]; /* GREEK CAPITAL LETTER ALPHA WITH PSILI AND PROSGEGRAMMENI */ +$config['1f00_1fff'][] = ['upper' => 8073, 'status' => 'F', 'lower' => [7937, 953]]; /* GREEK CAPITAL LETTER ALPHA WITH DASIA AND PROSGEGRAMMENI */ +$config['1f00_1fff'][] = ['upper' => 8073, 'status' => 'S', 'lower' => [8065]]; /* GREEK CAPITAL LETTER ALPHA WITH DASIA AND PROSGEGRAMMENI */ +$config['1f00_1fff'][] = ['upper' => 8074, 'status' => 'F', 'lower' => [7938, 953]]; /* GREEK CAPITAL LETTER ALPHA WITH PSILI AND VARIA AND PROSGEGRAMMENI */ +$config['1f00_1fff'][] = ['upper' => 8074, 'status' => 'S', 'lower' => [8066]]; /* GREEK CAPITAL LETTER ALPHA WITH PSILI AND VARIA AND PROSGEGRAMMENI */ +$config['1f00_1fff'][] = ['upper' => 8075, 'status' => 'F', 'lower' => [7939, 953]]; /* GREEK CAPITAL LETTER ALPHA WITH DASIA AND VARIA AND PROSGEGRAMMENI */ +$config['1f00_1fff'][] = ['upper' => 8075, 'status' => 'S', 'lower' => [8067]]; /* GREEK CAPITAL LETTER ALPHA WITH DASIA AND VARIA AND PROSGEGRAMMENI */ +$config['1f00_1fff'][] = ['upper' => 8076, 'status' => 'F', 'lower' => [7940, 953]]; /* GREEK CAPITAL LETTER ALPHA WITH PSILI AND OXIA AND PROSGEGRAMMENI */ +$config['1f00_1fff'][] = ['upper' => 8076, 'status' => 'S', 'lower' => [8068]]; /* GREEK CAPITAL LETTER ALPHA WITH PSILI AND OXIA AND PROSGEGRAMMENI */ +$config['1f00_1fff'][] = ['upper' => 8077, 'status' => 'F', 'lower' => [7941, 953]]; /* GREEK CAPITAL LETTER ALPHA WITH DASIA AND OXIA AND PROSGEGRAMMENI */ +$config['1f00_1fff'][] = ['upper' => 8077, 'status' => 'S', 'lower' => [8069]]; /* GREEK CAPITAL LETTER ALPHA WITH DASIA AND OXIA AND PROSGEGRAMMENI */ +$config['1f00_1fff'][] = ['upper' => 8078, 'status' => 'F', 'lower' => [7942, 953]]; /* GREEK CAPITAL LETTER ALPHA WITH PSILI AND PERISPOMENI AND PROSGEGRAMMENI */ +$config['1f00_1fff'][] = ['upper' => 8078, 'status' => 'S', 'lower' => [8070]]; /* GREEK CAPITAL LETTER ALPHA WITH PSILI AND PERISPOMENI AND PROSGEGRAMMENI */ +$config['1f00_1fff'][] = ['upper' => 8079, 'status' => 'F', 'lower' => [7943, 953]]; /* GREEK CAPITAL LETTER ALPHA WITH DASIA AND PERISPOMENI AND PROSGEGRAMMENI */ +$config['1f00_1fff'][] = ['upper' => 8079, 'status' => 'S', 'lower' => [8071]]; /* GREEK CAPITAL LETTER ALPHA WITH DASIA AND PERISPOMENI AND PROSGEGRAMMENI */ +$config['1f00_1fff'][] = ['upper' => 8080, 'status' => 'F', 'lower' => [7968, 953]]; /* GREEK SMALL LETTER ETA WITH PSILI AND YPOGEGRAMMENI */ +$config['1f00_1fff'][] = ['upper' => 8081, 'status' => 'F', 'lower' => [7969, 953]]; /* GREEK SMALL LETTER ETA WITH DASIA AND YPOGEGRAMMENI */ +$config['1f00_1fff'][] = ['upper' => 8082, 'status' => 'F', 'lower' => [7970, 953]]; /* GREEK SMALL LETTER ETA WITH PSILI AND VARIA AND YPOGEGRAMMENI */ +$config['1f00_1fff'][] = ['upper' => 8083, 'status' => 'F', 'lower' => [7971, 953]]; /* GREEK SMALL LETTER ETA WITH DASIA AND VARIA AND YPOGEGRAMMENI */ +$config['1f00_1fff'][] = ['upper' => 8084, 'status' => 'F', 'lower' => [7972, 953]]; /* GREEK SMALL LETTER ETA WITH PSILI AND OXIA AND YPOGEGRAMMENI */ +$config['1f00_1fff'][] = ['upper' => 8085, 'status' => 'F', 'lower' => [7973, 953]]; /* GREEK SMALL LETTER ETA WITH DASIA AND OXIA AND YPOGEGRAMMENI */ +$config['1f00_1fff'][] = ['upper' => 8086, 'status' => 'F', 'lower' => [7974, 953]]; /* GREEK SMALL LETTER ETA WITH PSILI AND PERISPOMENI AND YPOGEGRAMMENI */ +$config['1f00_1fff'][] = ['upper' => 8087, 'status' => 'F', 'lower' => [7975, 953]]; /* GREEK SMALL LETTER ETA WITH DASIA AND PERISPOMENI AND YPOGEGRAMMENI */ +$config['1f00_1fff'][] = ['upper' => 8088, 'status' => 'F', 'lower' => [7968, 953]]; /* GREEK CAPITAL LETTER ETA WITH PSILI AND PROSGEGRAMMENI */ +$config['1f00_1fff'][] = ['upper' => 8088, 'status' => 'S', 'lower' => [8080]]; /* GREEK CAPITAL LETTER ETA WITH PSILI AND PROSGEGRAMMENI */ +$config['1f00_1fff'][] = ['upper' => 8089, 'status' => 'F', 'lower' => [7969, 953]]; /* GREEK CAPITAL LETTER ETA WITH DASIA AND PROSGEGRAMMENI */ +$config['1f00_1fff'][] = ['upper' => 8089, 'status' => 'S', 'lower' => [8081]]; /* GREEK CAPITAL LETTER ETA WITH DASIA AND PROSGEGRAMMENI */ +$config['1f00_1fff'][] = ['upper' => 8090, 'status' => 'F', 'lower' => [7970, 953]]; /* GREEK CAPITAL LETTER ETA WITH PSILI AND VARIA AND PROSGEGRAMMENI */ +$config['1f00_1fff'][] = ['upper' => 8090, 'status' => 'S', 'lower' => [8082]]; /* GREEK CAPITAL LETTER ETA WITH PSILI AND VARIA AND PROSGEGRAMMENI */ +$config['1f00_1fff'][] = ['upper' => 8091, 'status' => 'F', 'lower' => [7971, 953]]; /* GREEK CAPITAL LETTER ETA WITH DASIA AND VARIA AND PROSGEGRAMMENI */ +$config['1f00_1fff'][] = ['upper' => 8091, 'status' => 'S', 'lower' => [8083]]; /* GREEK CAPITAL LETTER ETA WITH DASIA AND VARIA AND PROSGEGRAMMENI */ +$config['1f00_1fff'][] = ['upper' => 8092, 'status' => 'F', 'lower' => [7972, 953]]; /* GREEK CAPITAL LETTER ETA WITH PSILI AND OXIA AND PROSGEGRAMMENI */ +$config['1f00_1fff'][] = ['upper' => 8092, 'status' => 'S', 'lower' => [8084]]; /* GREEK CAPITAL LETTER ETA WITH PSILI AND OXIA AND PROSGEGRAMMENI */ +$config['1f00_1fff'][] = ['upper' => 8093, 'status' => 'F', 'lower' => [7973, 953]]; /* GREEK CAPITAL LETTER ETA WITH DASIA AND OXIA AND PROSGEGRAMMENI */ +$config['1f00_1fff'][] = ['upper' => 8093, 'status' => 'S', 'lower' => [8085]]; /* GREEK CAPITAL LETTER ETA WITH DASIA AND OXIA AND PROSGEGRAMMENI */ +$config['1f00_1fff'][] = ['upper' => 8094, 'status' => 'F', 'lower' => [7974, 953]]; /* GREEK CAPITAL LETTER ETA WITH PSILI AND PERISPOMENI AND PROSGEGRAMMENI */ +$config['1f00_1fff'][] = ['upper' => 8094, 'status' => 'S', 'lower' => [8086]]; /* GREEK CAPITAL LETTER ETA WITH PSILI AND PERISPOMENI AND PROSGEGRAMMENI */ +$config['1f00_1fff'][] = ['upper' => 8095, 'status' => 'F', 'lower' => [7975, 953]]; /* GREEK CAPITAL LETTER ETA WITH DASIA AND PERISPOMENI AND PROSGEGRAMMENI */ +$config['1f00_1fff'][] = ['upper' => 8095, 'status' => 'S', 'lower' => [8087]]; /* GREEK CAPITAL LETTER ETA WITH DASIA AND PERISPOMENI AND PROSGEGRAMMENI */ +$config['1f00_1fff'][] = ['upper' => 8096, 'status' => 'F', 'lower' => [8032, 953]]; /* GREEK SMALL LETTER OMEGA WITH PSILI AND YPOGEGRAMMENI */ +$config['1f00_1fff'][] = ['upper' => 8097, 'status' => 'F', 'lower' => [8033, 953]]; /* GREEK SMALL LETTER OMEGA WITH DASIA AND YPOGEGRAMMENI */ +$config['1f00_1fff'][] = ['upper' => 8098, 'status' => 'F', 'lower' => [8034, 953]]; /* GREEK SMALL LETTER OMEGA WITH PSILI AND VARIA AND YPOGEGRAMMENI */ +$config['1f00_1fff'][] = ['upper' => 8099, 'status' => 'F', 'lower' => [8035, 953]]; /* GREEK SMALL LETTER OMEGA WITH DASIA AND VARIA AND YPOGEGRAMMENI */ +$config['1f00_1fff'][] = ['upper' => 8100, 'status' => 'F', 'lower' => [8036, 953]]; /* GREEK SMALL LETTER OMEGA WITH PSILI AND OXIA AND YPOGEGRAMMENI */ +$config['1f00_1fff'][] = ['upper' => 8101, 'status' => 'F', 'lower' => [8037, 953]]; /* GREEK SMALL LETTER OMEGA WITH DASIA AND OXIA AND YPOGEGRAMMENI */ +$config['1f00_1fff'][] = ['upper' => 8102, 'status' => 'F', 'lower' => [8038, 953]]; /* GREEK SMALL LETTER OMEGA WITH PSILI AND PERISPOMENI AND YPOGEGRAMMENI */ +$config['1f00_1fff'][] = ['upper' => 8103, 'status' => 'F', 'lower' => [8039, 953]]; /* GREEK SMALL LETTER OMEGA WITH DASIA AND PERISPOMENI AND YPOGEGRAMMENI */ +$config['1f00_1fff'][] = ['upper' => 8104, 'status' => 'F', 'lower' => [8032, 953]]; /* GREEK CAPITAL LETTER OMEGA WITH PSILI AND PROSGEGRAMMENI */ +$config['1f00_1fff'][] = ['upper' => 8104, 'status' => 'S', 'lower' => [8096]]; /* GREEK CAPITAL LETTER OMEGA WITH PSILI AND PROSGEGRAMMENI */ +$config['1f00_1fff'][] = ['upper' => 8105, 'status' => 'F', 'lower' => [8033, 953]]; /* GREEK CAPITAL LETTER OMEGA WITH DASIA AND PROSGEGRAMMENI */ +$config['1f00_1fff'][] = ['upper' => 8105, 'status' => 'S', 'lower' => [8097]]; /* GREEK CAPITAL LETTER OMEGA WITH DASIA AND PROSGEGRAMMENI */ +$config['1f00_1fff'][] = ['upper' => 8106, 'status' => 'F', 'lower' => [8034, 953]]; /* GREEK CAPITAL LETTER OMEGA WITH PSILI AND VARIA AND PROSGEGRAMMENI */ +$config['1f00_1fff'][] = ['upper' => 8106, 'status' => 'S', 'lower' => [8098]]; /* GREEK CAPITAL LETTER OMEGA WITH PSILI AND VARIA AND PROSGEGRAMMENI */ +$config['1f00_1fff'][] = ['upper' => 8107, 'status' => 'F', 'lower' => [8035, 953]]; /* GREEK CAPITAL LETTER OMEGA WITH DASIA AND VARIA AND PROSGEGRAMMENI */ +$config['1f00_1fff'][] = ['upper' => 8107, 'status' => 'S', 'lower' => [8099]]; /* GREEK CAPITAL LETTER OMEGA WITH DASIA AND VARIA AND PROSGEGRAMMENI */ +$config['1f00_1fff'][] = ['upper' => 8108, 'status' => 'F', 'lower' => [8036, 953]]; /* GREEK CAPITAL LETTER OMEGA WITH PSILI AND OXIA AND PROSGEGRAMMENI */ +$config['1f00_1fff'][] = ['upper' => 8108, 'status' => 'S', 'lower' => [8100]]; /* GREEK CAPITAL LETTER OMEGA WITH PSILI AND OXIA AND PROSGEGRAMMENI */ +$config['1f00_1fff'][] = ['upper' => 8109, 'status' => 'F', 'lower' => [8037, 953]]; /* GREEK CAPITAL LETTER OMEGA WITH DASIA AND OXIA AND PROSGEGRAMMENI */ +$config['1f00_1fff'][] = ['upper' => 8109, 'status' => 'S', 'lower' => [8101]]; /* GREEK CAPITAL LETTER OMEGA WITH DASIA AND OXIA AND PROSGEGRAMMENI */ +$config['1f00_1fff'][] = ['upper' => 8110, 'status' => 'F', 'lower' => [8038, 953]]; /* GREEK CAPITAL LETTER OMEGA WITH PSILI AND PERISPOMENI AND PROSGEGRAMMENI */ +$config['1f00_1fff'][] = ['upper' => 8110, 'status' => 'S', 'lower' => [8102]]; /* GREEK CAPITAL LETTER OMEGA WITH PSILI AND PERISPOMENI AND PROSGEGRAMMENI */ +$config['1f00_1fff'][] = ['upper' => 8111, 'status' => 'F', 'lower' => [8039, 953]]; /* GREEK CAPITAL LETTER OMEGA WITH DASIA AND PERISPOMENI AND PROSGEGRAMMENI */ +$config['1f00_1fff'][] = ['upper' => 8111, 'status' => 'S', 'lower' => [8103]]; /* GREEK CAPITAL LETTER OMEGA WITH DASIA AND PERISPOMENI AND PROSGEGRAMMENI */ +$config['1f00_1fff'][] = ['upper' => 8114, 'status' => 'F', 'lower' => [8048, 953]]; /* GREEK SMALL LETTER ALPHA WITH VARIA AND YPOGEGRAMMENI */ +$config['1f00_1fff'][] = ['upper' => 8115, 'status' => 'F', 'lower' => [945, 953]]; /* GREEK SMALL LETTER ALPHA WITH YPOGEGRAMMENI */ +$config['1f00_1fff'][] = ['upper' => 8116, 'status' => 'F', 'lower' => [940, 953]]; /* GREEK SMALL LETTER ALPHA WITH OXIA AND YPOGEGRAMMENI */ +$config['1f00_1fff'][] = ['upper' => 8118, 'status' => 'F', 'lower' => [945, 834]]; /* GREEK SMALL LETTER ALPHA WITH PERISPOMENI */ +$config['1f00_1fff'][] = ['upper' => 8119, 'status' => 'F', 'lower' => [945, 834, 953]]; /* GREEK SMALL LETTER ALPHA WITH PERISPOMENI AND YPOGEGRAMMENI */ +$config['1f00_1fff'][] = ['upper' => 8120, 'status' => 'C', 'lower' => [8112]]; /* GREEK CAPITAL LETTER ALPHA WITH VRACHY */ +$config['1f00_1fff'][] = ['upper' => 8121, 'status' => 'C', 'lower' => [8113]]; /* GREEK CAPITAL LETTER ALPHA WITH MACRON */ +$config['1f00_1fff'][] = ['upper' => 8122, 'status' => 'C', 'lower' => [8048]]; /* GREEK CAPITAL LETTER ALPHA WITH VARIA */ +$config['1f00_1fff'][] = ['upper' => 8123, 'status' => 'C', 'lower' => [8049]]; /* GREEK CAPITAL LETTER ALPHA WITH OXIA */ +$config['1f00_1fff'][] = ['upper' => 8124, 'status' => 'F', 'lower' => [945, 953]]; /* GREEK CAPITAL LETTER ALPHA WITH PROSGEGRAMMENI */ +$config['1f00_1fff'][] = ['upper' => 8124, 'status' => 'S', 'lower' => [8115]]; /* GREEK CAPITAL LETTER ALPHA WITH PROSGEGRAMMENI */ +$config['1f00_1fff'][] = ['upper' => 8126, 'status' => 'C', 'lower' => [953]]; /* GREEK PROSGEGRAMMENI */ +$config['1f00_1fff'][] = ['upper' => 8130, 'status' => 'F', 'lower' => [8052, 953]]; /* GREEK SMALL LETTER ETA WITH VARIA AND YPOGEGRAMMENI */ +$config['1f00_1fff'][] = ['upper' => 8131, 'status' => 'F', 'lower' => [951, 953]]; /* GREEK SMALL LETTER ETA WITH YPOGEGRAMMENI */ +$config['1f00_1fff'][] = ['upper' => 8132, 'status' => 'F', 'lower' => [942, 953]]; /* GREEK SMALL LETTER ETA WITH OXIA AND YPOGEGRAMMENI */ +$config['1f00_1fff'][] = ['upper' => 8134, 'status' => 'F', 'lower' => [951, 834]]; /* GREEK SMALL LETTER ETA WITH PERISPOMENI */ +$config['1f00_1fff'][] = ['upper' => 8135, 'status' => 'F', 'lower' => [951, 834, 953]]; /* GREEK SMALL LETTER ETA WITH PERISPOMENI AND YPOGEGRAMMENI */ +$config['1f00_1fff'][] = ['upper' => 8136, 'status' => 'C', 'lower' => [8050]]; /* GREEK CAPITAL LETTER EPSILON WITH VARIA */ +$config['1f00_1fff'][] = ['upper' => 8137, 'status' => 'C', 'lower' => [8051]]; /* GREEK CAPITAL LETTER EPSILON WITH OXIA */ +$config['1f00_1fff'][] = ['upper' => 8138, 'status' => 'C', 'lower' => [8052]]; /* GREEK CAPITAL LETTER ETA WITH VARIA */ +$config['1f00_1fff'][] = ['upper' => 8139, 'status' => 'C', 'lower' => [8053]]; /* GREEK CAPITAL LETTER ETA WITH OXIA */ +$config['1f00_1fff'][] = ['upper' => 8140, 'status' => 'F', 'lower' => [951, 953]]; /* GREEK CAPITAL LETTER ETA WITH PROSGEGRAMMENI */ +$config['1f00_1fff'][] = ['upper' => 8140, 'status' => 'S', 'lower' => [8131]]; /* GREEK CAPITAL LETTER ETA WITH PROSGEGRAMMENI */ +$config['1f00_1fff'][] = ['upper' => 8146, 'status' => 'F', 'lower' => [953, 776, 768]]; /* GREEK SMALL LETTER IOTA WITH DIALYTIKA AND VARIA */ +$config['1f00_1fff'][] = ['upper' => 8147, 'status' => 'F', 'lower' => [953, 776, 769]]; /* GREEK SMALL LETTER IOTA WITH DIALYTIKA AND OXIA */ +$config['1f00_1fff'][] = ['upper' => 8150, 'status' => 'F', 'lower' => [953, 834]]; /* GREEK SMALL LETTER IOTA WITH PERISPOMENI */ +$config['1f00_1fff'][] = ['upper' => 8151, 'status' => 'F', 'lower' => [953, 776, 834]]; /* GREEK SMALL LETTER IOTA WITH DIALYTIKA AND PERISPOMENI */ +$config['1f00_1fff'][] = ['upper' => 8152, 'status' => 'C', 'lower' => [8144]]; /* GREEK CAPITAL LETTER IOTA WITH VRACHY */ +$config['1f00_1fff'][] = ['upper' => 8153, 'status' => 'C', 'lower' => [8145]]; /* GREEK CAPITAL LETTER IOTA WITH MACRON */ +$config['1f00_1fff'][] = ['upper' => 8154, 'status' => 'C', 'lower' => [8054]]; /* GREEK CAPITAL LETTER IOTA WITH VARIA */ +$config['1f00_1fff'][] = ['upper' => 8155, 'status' => 'C', 'lower' => [8055]]; /* GREEK CAPITAL LETTER IOTA WITH OXIA */ +$config['1f00_1fff'][] = ['upper' => 8162, 'status' => 'F', 'lower' => [965, 776, 768]]; /* GREEK SMALL LETTER UPSILON WITH DIALYTIKA AND VARIA */ +$config['1f00_1fff'][] = ['upper' => 8163, 'status' => 'F', 'lower' => [965, 776, 769]]; /* GREEK SMALL LETTER UPSILON WITH DIALYTIKA AND OXIA */ +$config['1f00_1fff'][] = ['upper' => 8164, 'status' => 'F', 'lower' => [961, 787]]; /* GREEK SMALL LETTER RHO WITH PSILI */ +$config['1f00_1fff'][] = ['upper' => 8166, 'status' => 'F', 'lower' => [965, 834]]; /* GREEK SMALL LETTER UPSILON WITH PERISPOMENI */ +$config['1f00_1fff'][] = ['upper' => 8167, 'status' => 'F', 'lower' => [965, 776, 834]]; /* GREEK SMALL LETTER UPSILON WITH DIALYTIKA AND PERISPOMENI */ +$config['1f00_1fff'][] = ['upper' => 8168, 'status' => 'C', 'lower' => [8160]]; /* GREEK CAPITAL LETTER UPSILON WITH VRACHY */ +$config['1f00_1fff'][] = ['upper' => 8169, 'status' => 'C', 'lower' => [8161]]; /* GREEK CAPITAL LETTER UPSILON WITH MACRON */ +$config['1f00_1fff'][] = ['upper' => 8170, 'status' => 'C', 'lower' => [8058]]; /* GREEK CAPITAL LETTER UPSILON WITH VARIA */ +$config['1f00_1fff'][] = ['upper' => 8171, 'status' => 'C', 'lower' => [8059]]; /* GREEK CAPITAL LETTER UPSILON WITH OXIA */ +$config['1f00_1fff'][] = ['upper' => 8172, 'status' => 'C', 'lower' => [8165]]; /* GREEK CAPITAL LETTER RHO WITH DASIA */ +$config['1f00_1fff'][] = ['upper' => 8178, 'status' => 'F', 'lower' => [8060, 953]]; /* GREEK SMALL LETTER OMEGA WITH VARIA AND YPOGEGRAMMENI */ +$config['1f00_1fff'][] = ['upper' => 8179, 'status' => 'F', 'lower' => [969, 953]]; /* GREEK SMALL LETTER OMEGA WITH YPOGEGRAMMENI */ +$config['1f00_1fff'][] = ['upper' => 8180, 'status' => 'F', 'lower' => [974, 953]]; /* GREEK SMALL LETTER OMEGA WITH OXIA AND YPOGEGRAMMENI */ +$config['1f00_1fff'][] = ['upper' => 8182, 'status' => 'F', 'lower' => [969, 834]]; /* GREEK SMALL LETTER OMEGA WITH PERISPOMENI */ +$config['1f00_1fff'][] = ['upper' => 8183, 'status' => 'F', 'lower' => [969, 834, 953]]; /* GREEK SMALL LETTER OMEGA WITH PERISPOMENI AND YPOGEGRAMMENI */ +$config['1f00_1fff'][] = ['upper' => 8184, 'status' => 'C', 'lower' => [8056]]; /* GREEK CAPITAL LETTER OMICRON WITH VARIA */ +$config['1f00_1fff'][] = ['upper' => 8185, 'status' => 'C', 'lower' => [8057]]; /* GREEK CAPITAL LETTER OMICRON WITH OXIA */ +$config['1f00_1fff'][] = ['upper' => 8186, 'status' => 'C', 'lower' => [8060]]; /* GREEK CAPITAL LETTER OMEGA WITH VARIA */ +$config['1f00_1fff'][] = ['upper' => 8187, 'status' => 'C', 'lower' => [8061]]; /* GREEK CAPITAL LETTER OMEGA WITH OXIA */ +$config['1f00_1fff'][] = ['upper' => 8188, 'status' => 'F', 'lower' => [969, 953]]; /* GREEK CAPITAL LETTER OMEGA WITH PROSGEGRAMMENI */ +$config['1f00_1fff'][] = ['upper' => 8188, 'status' => 'S', 'lower' => [8179]]; /* GREEK CAPITAL LETTER OMEGA WITH PROSGEGRAMMENI */ diff --git a/lib/Cake/Config/unicode/casefolding/2100_214f.php b/lib/Cake/Config/unicode/casefolding/2100_214f.php index 9e409805..65e9c576 100755 --- a/lib/Cake/Config/unicode/casefolding/2100_214f.php +++ b/lib/Cake/Config/unicode/casefolding/2100_214f.php @@ -27,7 +27,7 @@ * * The lower filed is an array of the decimal values that form the lower case version of a character. * - * The status field is: + * The status field is: * C: common case folding, common mappings shared by both simple and full mappings. * F: full case folding, mappings that cause strings to grow in length. Multiple characters are separated by spaces. * S: simple case folding, mappings to single characters where different from F. @@ -37,7 +37,7 @@ * Note that the Turkic mappings do not maintain canonical equivalence without additional processing. * See the discussions of case mapping in the Unicode Standard for more information. */ -$config['2100_214f'][] = array('upper' => 8486, 'status' => 'C', 'lower' => array(969)); /* OHM SIGN */ -$config['2100_214f'][] = array('upper' => 8490, 'status' => 'C', 'lower' => array(107)); /* KELVIN SIGN */ -$config['2100_214f'][] = array('upper' => 8491, 'status' => 'C', 'lower' => array(229)); /* ANGSTROM SIGN */ -$config['2100_214f'][] = array('upper' => 8498, 'status' => 'C', 'lower' => array(8526)); /* TURNED CAPITAL F */ +$config['2100_214f'][] = ['upper' => 8486, 'status' => 'C', 'lower' => [969]]; /* OHM SIGN */ +$config['2100_214f'][] = ['upper' => 8490, 'status' => 'C', 'lower' => [107]]; /* KELVIN SIGN */ +$config['2100_214f'][] = ['upper' => 8491, 'status' => 'C', 'lower' => [229]]; /* ANGSTROM SIGN */ +$config['2100_214f'][] = ['upper' => 8498, 'status' => 'C', 'lower' => [8526]]; /* TURNED CAPITAL F */ diff --git a/lib/Cake/Config/unicode/casefolding/2150_218f.php b/lib/Cake/Config/unicode/casefolding/2150_218f.php index 119121e2..a0f10328 100755 --- a/lib/Cake/Config/unicode/casefolding/2150_218f.php +++ b/lib/Cake/Config/unicode/casefolding/2150_218f.php @@ -27,7 +27,7 @@ * * The lower filed is an array of the decimal values that form the lower case version of a character. * - * The status field is: + * The status field is: * C: common case folding, common mappings shared by both simple and full mappings. * F: full case folding, mappings that cause strings to grow in length. Multiple characters are separated by spaces. * S: simple case folding, mappings to single characters where different from F. @@ -37,20 +37,20 @@ * Note that the Turkic mappings do not maintain canonical equivalence without additional processing. * See the discussions of case mapping in the Unicode Standard for more information. */ -$config['2150_218f'][] = array('upper' => 8544, 'status' => 'C', 'lower' => array(8560)); /* ROMAN NUMERAL ONE */ -$config['2150_218f'][] = array('upper' => 8545, 'status' => 'C', 'lower' => array(8561)); /* ROMAN NUMERAL TWO */ -$config['2150_218f'][] = array('upper' => 8546, 'status' => 'C', 'lower' => array(8562)); /* ROMAN NUMERAL THREE */ -$config['2150_218f'][] = array('upper' => 8547, 'status' => 'C', 'lower' => array(8563)); /* ROMAN NUMERAL FOUR */ -$config['2150_218f'][] = array('upper' => 8548, 'status' => 'C', 'lower' => array(8564)); /* ROMAN NUMERAL FIVE */ -$config['2150_218f'][] = array('upper' => 8549, 'status' => 'C', 'lower' => array(8565)); /* ROMAN NUMERAL SIX */ -$config['2150_218f'][] = array('upper' => 8550, 'status' => 'C', 'lower' => array(8566)); /* ROMAN NUMERAL SEVEN */ -$config['2150_218f'][] = array('upper' => 8551, 'status' => 'C', 'lower' => array(8567)); /* ROMAN NUMERAL EIGHT */ -$config['2150_218f'][] = array('upper' => 8552, 'status' => 'C', 'lower' => array(8568)); /* ROMAN NUMERAL NINE */ -$config['2150_218f'][] = array('upper' => 8553, 'status' => 'C', 'lower' => array(8569)); /* ROMAN NUMERAL TEN */ -$config['2150_218f'][] = array('upper' => 8554, 'status' => 'C', 'lower' => array(8570)); /* ROMAN NUMERAL ELEVEN */ -$config['2150_218f'][] = array('upper' => 8555, 'status' => 'C', 'lower' => array(8571)); /* ROMAN NUMERAL TWELVE */ -$config['2150_218f'][] = array('upper' => 8556, 'status' => 'C', 'lower' => array(8572)); /* ROMAN NUMERAL FIFTY */ -$config['2150_218f'][] = array('upper' => 8557, 'status' => 'C', 'lower' => array(8573)); /* ROMAN NUMERAL ONE HUNDRED */ -$config['2150_218f'][] = array('upper' => 8558, 'status' => 'C', 'lower' => array(8574)); /* ROMAN NUMERAL FIVE HUNDRED */ -$config['2150_218f'][] = array('upper' => 8559, 'status' => 'C', 'lower' => array(8575)); /* ROMAN NUMERAL ONE THOUSAND */ -$config['2150_218f'][] = array('upper' => 8579, 'status' => 'C', 'lower' => array(8580)); /* ROMAN NUMERAL REVERSED ONE HUNDRED */ +$config['2150_218f'][] = ['upper' => 8544, 'status' => 'C', 'lower' => [8560]]; /* ROMAN NUMERAL ONE */ +$config['2150_218f'][] = ['upper' => 8545, 'status' => 'C', 'lower' => [8561]]; /* ROMAN NUMERAL TWO */ +$config['2150_218f'][] = ['upper' => 8546, 'status' => 'C', 'lower' => [8562]]; /* ROMAN NUMERAL THREE */ +$config['2150_218f'][] = ['upper' => 8547, 'status' => 'C', 'lower' => [8563]]; /* ROMAN NUMERAL FOUR */ +$config['2150_218f'][] = ['upper' => 8548, 'status' => 'C', 'lower' => [8564]]; /* ROMAN NUMERAL FIVE */ +$config['2150_218f'][] = ['upper' => 8549, 'status' => 'C', 'lower' => [8565]]; /* ROMAN NUMERAL SIX */ +$config['2150_218f'][] = ['upper' => 8550, 'status' => 'C', 'lower' => [8566]]; /* ROMAN NUMERAL SEVEN */ +$config['2150_218f'][] = ['upper' => 8551, 'status' => 'C', 'lower' => [8567]]; /* ROMAN NUMERAL EIGHT */ +$config['2150_218f'][] = ['upper' => 8552, 'status' => 'C', 'lower' => [8568]]; /* ROMAN NUMERAL NINE */ +$config['2150_218f'][] = ['upper' => 8553, 'status' => 'C', 'lower' => [8569]]; /* ROMAN NUMERAL TEN */ +$config['2150_218f'][] = ['upper' => 8554, 'status' => 'C', 'lower' => [8570]]; /* ROMAN NUMERAL ELEVEN */ +$config['2150_218f'][] = ['upper' => 8555, 'status' => 'C', 'lower' => [8571]]; /* ROMAN NUMERAL TWELVE */ +$config['2150_218f'][] = ['upper' => 8556, 'status' => 'C', 'lower' => [8572]]; /* ROMAN NUMERAL FIFTY */ +$config['2150_218f'][] = ['upper' => 8557, 'status' => 'C', 'lower' => [8573]]; /* ROMAN NUMERAL ONE HUNDRED */ +$config['2150_218f'][] = ['upper' => 8558, 'status' => 'C', 'lower' => [8574]]; /* ROMAN NUMERAL FIVE HUNDRED */ +$config['2150_218f'][] = ['upper' => 8559, 'status' => 'C', 'lower' => [8575]]; /* ROMAN NUMERAL ONE THOUSAND */ +$config['2150_218f'][] = ['upper' => 8579, 'status' => 'C', 'lower' => [8580]]; /* ROMAN NUMERAL REVERSED ONE HUNDRED */ diff --git a/lib/Cake/Config/unicode/casefolding/2460_24ff.php b/lib/Cake/Config/unicode/casefolding/2460_24ff.php index d27d0e01..be5a0dcd 100755 --- a/lib/Cake/Config/unicode/casefolding/2460_24ff.php +++ b/lib/Cake/Config/unicode/casefolding/2460_24ff.php @@ -27,7 +27,7 @@ * * The lower filed is an array of the decimal values that form the lower case version of a character. * - * The status field is: + * The status field is: * C: common case folding, common mappings shared by both simple and full mappings. * F: full case folding, mappings that cause strings to grow in length. Multiple characters are separated by spaces. * S: simple case folding, mappings to single characters where different from F. @@ -37,29 +37,29 @@ * Note that the Turkic mappings do not maintain canonical equivalence without additional processing. * See the discussions of case mapping in the Unicode Standard for more information. */ -$config['2460_24ff'][] = array('upper' => 9398, 'status' => 'C', 'lower' => array(9424)); /* CIRCLED LATIN CAPITAL LETTER A */ -$config['2460_24ff'][] = array('upper' => 9399, 'status' => 'C', 'lower' => array(9425)); /* CIRCLED LATIN CAPITAL LETTER B */ -$config['2460_24ff'][] = array('upper' => 9400, 'status' => 'C', 'lower' => array(9426)); /* CIRCLED LATIN CAPITAL LETTER C */ -$config['2460_24ff'][] = array('upper' => 9401, 'status' => 'C', 'lower' => array(9427)); /* CIRCLED LATIN CAPITAL LETTER D */ -$config['2460_24ff'][] = array('upper' => 9402, 'status' => 'C', 'lower' => array(9428)); /* CIRCLED LATIN CAPITAL LETTER E */ -$config['2460_24ff'][] = array('upper' => 9403, 'status' => 'C', 'lower' => array(9429)); /* CIRCLED LATIN CAPITAL LETTER F */ -$config['2460_24ff'][] = array('upper' => 9404, 'status' => 'C', 'lower' => array(9430)); /* CIRCLED LATIN CAPITAL LETTER G */ -$config['2460_24ff'][] = array('upper' => 9405, 'status' => 'C', 'lower' => array(9431)); /* CIRCLED LATIN CAPITAL LETTER H */ -$config['2460_24ff'][] = array('upper' => 9406, 'status' => 'C', 'lower' => array(9432)); /* CIRCLED LATIN CAPITAL LETTER I */ -$config['2460_24ff'][] = array('upper' => 9407, 'status' => 'C', 'lower' => array(9433)); /* CIRCLED LATIN CAPITAL LETTER J */ -$config['2460_24ff'][] = array('upper' => 9408, 'status' => 'C', 'lower' => array(9434)); /* CIRCLED LATIN CAPITAL LETTER K */ -$config['2460_24ff'][] = array('upper' => 9409, 'status' => 'C', 'lower' => array(9435)); /* CIRCLED LATIN CAPITAL LETTER L */ -$config['2460_24ff'][] = array('upper' => 9410, 'status' => 'C', 'lower' => array(9436)); /* CIRCLED LATIN CAPITAL LETTER M */ -$config['2460_24ff'][] = array('upper' => 9411, 'status' => 'C', 'lower' => array(9437)); /* CIRCLED LATIN CAPITAL LETTER N */ -$config['2460_24ff'][] = array('upper' => 9412, 'status' => 'C', 'lower' => array(9438)); /* CIRCLED LATIN CAPITAL LETTER O */ -$config['2460_24ff'][] = array('upper' => 9413, 'status' => 'C', 'lower' => array(9439)); /* CIRCLED LATIN CAPITAL LETTER P */ -$config['2460_24ff'][] = array('upper' => 9414, 'status' => 'C', 'lower' => array(9440)); /* CIRCLED LATIN CAPITAL LETTER Q */ -$config['2460_24ff'][] = array('upper' => 9415, 'status' => 'C', 'lower' => array(9441)); /* CIRCLED LATIN CAPITAL LETTER R */ -$config['2460_24ff'][] = array('upper' => 9416, 'status' => 'C', 'lower' => array(9442)); /* CIRCLED LATIN CAPITAL LETTER S */ -$config['2460_24ff'][] = array('upper' => 9417, 'status' => 'C', 'lower' => array(9443)); /* CIRCLED LATIN CAPITAL LETTER T */ -$config['2460_24ff'][] = array('upper' => 9418, 'status' => 'C', 'lower' => array(9444)); /* CIRCLED LATIN CAPITAL LETTER U */ -$config['2460_24ff'][] = array('upper' => 9419, 'status' => 'C', 'lower' => array(9445)); /* CIRCLED LATIN CAPITAL LETTER V */ -$config['2460_24ff'][] = array('upper' => 9420, 'status' => 'C', 'lower' => array(9446)); /* CIRCLED LATIN CAPITAL LETTER W */ -$config['2460_24ff'][] = array('upper' => 9421, 'status' => 'C', 'lower' => array(9447)); /* CIRCLED LATIN CAPITAL LETTER X */ -$config['2460_24ff'][] = array('upper' => 9422, 'status' => 'C', 'lower' => array(9448)); /* CIRCLED LATIN CAPITAL LETTER Y */ -$config['2460_24ff'][] = array('upper' => 9423, 'status' => 'C', 'lower' => array(9449)); /* CIRCLED LATIN CAPITAL LETTER Z */ +$config['2460_24ff'][] = ['upper' => 9398, 'status' => 'C', 'lower' => [9424]]; /* CIRCLED LATIN CAPITAL LETTER A */ +$config['2460_24ff'][] = ['upper' => 9399, 'status' => 'C', 'lower' => [9425]]; /* CIRCLED LATIN CAPITAL LETTER B */ +$config['2460_24ff'][] = ['upper' => 9400, 'status' => 'C', 'lower' => [9426]]; /* CIRCLED LATIN CAPITAL LETTER C */ +$config['2460_24ff'][] = ['upper' => 9401, 'status' => 'C', 'lower' => [9427]]; /* CIRCLED LATIN CAPITAL LETTER D */ +$config['2460_24ff'][] = ['upper' => 9402, 'status' => 'C', 'lower' => [9428]]; /* CIRCLED LATIN CAPITAL LETTER E */ +$config['2460_24ff'][] = ['upper' => 9403, 'status' => 'C', 'lower' => [9429]]; /* CIRCLED LATIN CAPITAL LETTER F */ +$config['2460_24ff'][] = ['upper' => 9404, 'status' => 'C', 'lower' => [9430]]; /* CIRCLED LATIN CAPITAL LETTER G */ +$config['2460_24ff'][] = ['upper' => 9405, 'status' => 'C', 'lower' => [9431]]; /* CIRCLED LATIN CAPITAL LETTER H */ +$config['2460_24ff'][] = ['upper' => 9406, 'status' => 'C', 'lower' => [9432]]; /* CIRCLED LATIN CAPITAL LETTER I */ +$config['2460_24ff'][] = ['upper' => 9407, 'status' => 'C', 'lower' => [9433]]; /* CIRCLED LATIN CAPITAL LETTER J */ +$config['2460_24ff'][] = ['upper' => 9408, 'status' => 'C', 'lower' => [9434]]; /* CIRCLED LATIN CAPITAL LETTER K */ +$config['2460_24ff'][] = ['upper' => 9409, 'status' => 'C', 'lower' => [9435]]; /* CIRCLED LATIN CAPITAL LETTER L */ +$config['2460_24ff'][] = ['upper' => 9410, 'status' => 'C', 'lower' => [9436]]; /* CIRCLED LATIN CAPITAL LETTER M */ +$config['2460_24ff'][] = ['upper' => 9411, 'status' => 'C', 'lower' => [9437]]; /* CIRCLED LATIN CAPITAL LETTER N */ +$config['2460_24ff'][] = ['upper' => 9412, 'status' => 'C', 'lower' => [9438]]; /* CIRCLED LATIN CAPITAL LETTER O */ +$config['2460_24ff'][] = ['upper' => 9413, 'status' => 'C', 'lower' => [9439]]; /* CIRCLED LATIN CAPITAL LETTER P */ +$config['2460_24ff'][] = ['upper' => 9414, 'status' => 'C', 'lower' => [9440]]; /* CIRCLED LATIN CAPITAL LETTER Q */ +$config['2460_24ff'][] = ['upper' => 9415, 'status' => 'C', 'lower' => [9441]]; /* CIRCLED LATIN CAPITAL LETTER R */ +$config['2460_24ff'][] = ['upper' => 9416, 'status' => 'C', 'lower' => [9442]]; /* CIRCLED LATIN CAPITAL LETTER S */ +$config['2460_24ff'][] = ['upper' => 9417, 'status' => 'C', 'lower' => [9443]]; /* CIRCLED LATIN CAPITAL LETTER T */ +$config['2460_24ff'][] = ['upper' => 9418, 'status' => 'C', 'lower' => [9444]]; /* CIRCLED LATIN CAPITAL LETTER U */ +$config['2460_24ff'][] = ['upper' => 9419, 'status' => 'C', 'lower' => [9445]]; /* CIRCLED LATIN CAPITAL LETTER V */ +$config['2460_24ff'][] = ['upper' => 9420, 'status' => 'C', 'lower' => [9446]]; /* CIRCLED LATIN CAPITAL LETTER W */ +$config['2460_24ff'][] = ['upper' => 9421, 'status' => 'C', 'lower' => [9447]]; /* CIRCLED LATIN CAPITAL LETTER X */ +$config['2460_24ff'][] = ['upper' => 9422, 'status' => 'C', 'lower' => [9448]]; /* CIRCLED LATIN CAPITAL LETTER Y */ +$config['2460_24ff'][] = ['upper' => 9423, 'status' => 'C', 'lower' => [9449]]; /* CIRCLED LATIN CAPITAL LETTER Z */ diff --git a/lib/Cake/Config/unicode/casefolding/2c00_2c5f.php b/lib/Cake/Config/unicode/casefolding/2c00_2c5f.php index bb1492d9..e9d19d7e 100755 --- a/lib/Cake/Config/unicode/casefolding/2c00_2c5f.php +++ b/lib/Cake/Config/unicode/casefolding/2c00_2c5f.php @@ -27,7 +27,7 @@ * * The lower filed is an array of the decimal values that form the lower case version of a character. * - * The status field is: + * The status field is: * C: common case folding, common mappings shared by both simple and full mappings. * F: full case folding, mappings that cause strings to grow in length. Multiple characters are separated by spaces. * S: simple case folding, mappings to single characters where different from F. @@ -37,50 +37,50 @@ * Note that the Turkic mappings do not maintain canonical equivalence without additional processing. * See the discussions of case mapping in the Unicode Standard for more information. */ -$config['2c00_2c5f'][] = array('upper' => 11264, 'status' => 'C', 'lower' => array(11312)); /* GLAGOLITIC CAPITAL LETTER AZU */ -$config['2c00_2c5f'][] = array('upper' => 11265, 'status' => 'C', 'lower' => array(11313)); /* GLAGOLITIC CAPITAL LETTER BUKY */ -$config['2c00_2c5f'][] = array('upper' => 11266, 'status' => 'C', 'lower' => array(11314)); /* GLAGOLITIC CAPITAL LETTER VEDE */ -$config['2c00_2c5f'][] = array('upper' => 11267, 'status' => 'C', 'lower' => array(11315)); /* GLAGOLITIC CAPITAL LETTER GLAGOLI */ -$config['2c00_2c5f'][] = array('upper' => 11268, 'status' => 'C', 'lower' => array(11316)); /* GLAGOLITIC CAPITAL LETTER DOBRO */ -$config['2c00_2c5f'][] = array('upper' => 11269, 'status' => 'C', 'lower' => array(11317)); /* GLAGOLITIC CAPITAL LETTER YESTU */ -$config['2c00_2c5f'][] = array('upper' => 11270, 'status' => 'C', 'lower' => array(11318)); /* GLAGOLITIC CAPITAL LETTER ZHIVETE */ -$config['2c00_2c5f'][] = array('upper' => 11271, 'status' => 'C', 'lower' => array(11319)); /* GLAGOLITIC CAPITAL LETTER DZELO */ -$config['2c00_2c5f'][] = array('upper' => 11272, 'status' => 'C', 'lower' => array(11320)); /* GLAGOLITIC CAPITAL LETTER ZEMLJA */ -$config['2c00_2c5f'][] = array('upper' => 11273, 'status' => 'C', 'lower' => array(11321)); /* GLAGOLITIC CAPITAL LETTER IZHE */ -$config['2c00_2c5f'][] = array('upper' => 11274, 'status' => 'C', 'lower' => array(11322)); /* GLAGOLITIC CAPITAL LETTER INITIAL IZHE */ -$config['2c00_2c5f'][] = array('upper' => 11275, 'status' => 'C', 'lower' => array(11323)); /* GLAGOLITIC CAPITAL LETTER I */ -$config['2c00_2c5f'][] = array('upper' => 11276, 'status' => 'C', 'lower' => array(11324)); /* GLAGOLITIC CAPITAL LETTER DJERVI */ -$config['2c00_2c5f'][] = array('upper' => 11277, 'status' => 'C', 'lower' => array(11325)); /* GLAGOLITIC CAPITAL LETTER KAKO */ -$config['2c00_2c5f'][] = array('upper' => 11278, 'status' => 'C', 'lower' => array(11326)); /* GLAGOLITIC CAPITAL LETTER LJUDIJE */ -$config['2c00_2c5f'][] = array('upper' => 11279, 'status' => 'C', 'lower' => array(11327)); /* GLAGOLITIC CAPITAL LETTER MYSLITE */ -$config['2c00_2c5f'][] = array('upper' => 11280, 'status' => 'C', 'lower' => array(11328)); /* GLAGOLITIC CAPITAL LETTER NASHI */ -$config['2c00_2c5f'][] = array('upper' => 11281, 'status' => 'C', 'lower' => array(11329)); /* GLAGOLITIC CAPITAL LETTER ONU */ -$config['2c00_2c5f'][] = array('upper' => 11282, 'status' => 'C', 'lower' => array(11330)); /* GLAGOLITIC CAPITAL LETTER POKOJI */ -$config['2c00_2c5f'][] = array('upper' => 11283, 'status' => 'C', 'lower' => array(11331)); /* GLAGOLITIC CAPITAL LETTER RITSI */ -$config['2c00_2c5f'][] = array('upper' => 11284, 'status' => 'C', 'lower' => array(11332)); /* GLAGOLITIC CAPITAL LETTER SLOVO */ -$config['2c00_2c5f'][] = array('upper' => 11285, 'status' => 'C', 'lower' => array(11333)); /* GLAGOLITIC CAPITAL LETTER TVRIDO */ -$config['2c00_2c5f'][] = array('upper' => 11286, 'status' => 'C', 'lower' => array(11334)); /* GLAGOLITIC CAPITAL LETTER UKU */ -$config['2c00_2c5f'][] = array('upper' => 11287, 'status' => 'C', 'lower' => array(11335)); /* GLAGOLITIC CAPITAL LETTER FRITU */ -$config['2c00_2c5f'][] = array('upper' => 11288, 'status' => 'C', 'lower' => array(11336)); /* GLAGOLITIC CAPITAL LETTER HERU */ -$config['2c00_2c5f'][] = array('upper' => 11289, 'status' => 'C', 'lower' => array(11337)); /* GLAGOLITIC CAPITAL LETTER OTU */ -$config['2c00_2c5f'][] = array('upper' => 11290, 'status' => 'C', 'lower' => array(11338)); /* GLAGOLITIC CAPITAL LETTER PE */ -$config['2c00_2c5f'][] = array('upper' => 11291, 'status' => 'C', 'lower' => array(11339)); /* GLAGOLITIC CAPITAL LETTER SHTA */ -$config['2c00_2c5f'][] = array('upper' => 11292, 'status' => 'C', 'lower' => array(11340)); /* GLAGOLITIC CAPITAL LETTER TSI */ -$config['2c00_2c5f'][] = array('upper' => 11293, 'status' => 'C', 'lower' => array(11341)); /* GLAGOLITIC CAPITAL LETTER CHRIVI */ -$config['2c00_2c5f'][] = array('upper' => 11294, 'status' => 'C', 'lower' => array(11342)); /* GLAGOLITIC CAPITAL LETTER SHA */ -$config['2c00_2c5f'][] = array('upper' => 11295, 'status' => 'C', 'lower' => array(11343)); /* GLAGOLITIC CAPITAL LETTER YERU */ -$config['2c00_2c5f'][] = array('upper' => 11296, 'status' => 'C', 'lower' => array(11344)); /* GLAGOLITIC CAPITAL LETTER YERI */ -$config['2c00_2c5f'][] = array('upper' => 11297, 'status' => 'C', 'lower' => array(11345)); /* GLAGOLITIC CAPITAL LETTER YATI */ -$config['2c00_2c5f'][] = array('upper' => 11298, 'status' => 'C', 'lower' => array(11346)); /* GLAGOLITIC CAPITAL LETTER SPIDERY HA */ -$config['2c00_2c5f'][] = array('upper' => 11299, 'status' => 'C', 'lower' => array(11347)); /* GLAGOLITIC CAPITAL LETTER YU */ -$config['2c00_2c5f'][] = array('upper' => 11300, 'status' => 'C', 'lower' => array(11348)); /* GLAGOLITIC CAPITAL LETTER SMALL YUS */ -$config['2c00_2c5f'][] = array('upper' => 11301, 'status' => 'C', 'lower' => array(11349)); /* GLAGOLITIC CAPITAL LETTER SMALL YUS WITH TAIL */ -$config['2c00_2c5f'][] = array('upper' => 11302, 'status' => 'C', 'lower' => array(11350)); /* GLAGOLITIC CAPITAL LETTER YO */ -$config['2c00_2c5f'][] = array('upper' => 11303, 'status' => 'C', 'lower' => array(11351)); /* GLAGOLITIC CAPITAL LETTER IOTATED SMALL YUS */ -$config['2c00_2c5f'][] = array('upper' => 11304, 'status' => 'C', 'lower' => array(11352)); /* GLAGOLITIC CAPITAL LETTER BIG YUS */ -$config['2c00_2c5f'][] = array('upper' => 11305, 'status' => 'C', 'lower' => array(11353)); /* GLAGOLITIC CAPITAL LETTER IOTATED BIG YUS */ -$config['2c00_2c5f'][] = array('upper' => 11306, 'status' => 'C', 'lower' => array(11354)); /* GLAGOLITIC CAPITAL LETTER FITA */ -$config['2c00_2c5f'][] = array('upper' => 11307, 'status' => 'C', 'lower' => array(11355)); /* GLAGOLITIC CAPITAL LETTER IZHITSA */ -$config['2c00_2c5f'][] = array('upper' => 11308, 'status' => 'C', 'lower' => array(11356)); /* GLAGOLITIC CAPITAL LETTER SHTAPIC */ -$config['2c00_2c5f'][] = array('upper' => 11309, 'status' => 'C', 'lower' => array(11357)); /* GLAGOLITIC CAPITAL LETTER TROKUTASTI A */ -$config['2c00_2c5f'][] = array('upper' => 11310, 'status' => 'C', 'lower' => array(11358)); /* GLAGOLITIC CAPITAL LETTER LATINATE MYSLITE */ +$config['2c00_2c5f'][] = ['upper' => 11264, 'status' => 'C', 'lower' => [11312]]; /* GLAGOLITIC CAPITAL LETTER AZU */ +$config['2c00_2c5f'][] = ['upper' => 11265, 'status' => 'C', 'lower' => [11313]]; /* GLAGOLITIC CAPITAL LETTER BUKY */ +$config['2c00_2c5f'][] = ['upper' => 11266, 'status' => 'C', 'lower' => [11314]]; /* GLAGOLITIC CAPITAL LETTER VEDE */ +$config['2c00_2c5f'][] = ['upper' => 11267, 'status' => 'C', 'lower' => [11315]]; /* GLAGOLITIC CAPITAL LETTER GLAGOLI */ +$config['2c00_2c5f'][] = ['upper' => 11268, 'status' => 'C', 'lower' => [11316]]; /* GLAGOLITIC CAPITAL LETTER DOBRO */ +$config['2c00_2c5f'][] = ['upper' => 11269, 'status' => 'C', 'lower' => [11317]]; /* GLAGOLITIC CAPITAL LETTER YESTU */ +$config['2c00_2c5f'][] = ['upper' => 11270, 'status' => 'C', 'lower' => [11318]]; /* GLAGOLITIC CAPITAL LETTER ZHIVETE */ +$config['2c00_2c5f'][] = ['upper' => 11271, 'status' => 'C', 'lower' => [11319]]; /* GLAGOLITIC CAPITAL LETTER DZELO */ +$config['2c00_2c5f'][] = ['upper' => 11272, 'status' => 'C', 'lower' => [11320]]; /* GLAGOLITIC CAPITAL LETTER ZEMLJA */ +$config['2c00_2c5f'][] = ['upper' => 11273, 'status' => 'C', 'lower' => [11321]]; /* GLAGOLITIC CAPITAL LETTER IZHE */ +$config['2c00_2c5f'][] = ['upper' => 11274, 'status' => 'C', 'lower' => [11322]]; /* GLAGOLITIC CAPITAL LETTER INITIAL IZHE */ +$config['2c00_2c5f'][] = ['upper' => 11275, 'status' => 'C', 'lower' => [11323]]; /* GLAGOLITIC CAPITAL LETTER I */ +$config['2c00_2c5f'][] = ['upper' => 11276, 'status' => 'C', 'lower' => [11324]]; /* GLAGOLITIC CAPITAL LETTER DJERVI */ +$config['2c00_2c5f'][] = ['upper' => 11277, 'status' => 'C', 'lower' => [11325]]; /* GLAGOLITIC CAPITAL LETTER KAKO */ +$config['2c00_2c5f'][] = ['upper' => 11278, 'status' => 'C', 'lower' => [11326]]; /* GLAGOLITIC CAPITAL LETTER LJUDIJE */ +$config['2c00_2c5f'][] = ['upper' => 11279, 'status' => 'C', 'lower' => [11327]]; /* GLAGOLITIC CAPITAL LETTER MYSLITE */ +$config['2c00_2c5f'][] = ['upper' => 11280, 'status' => 'C', 'lower' => [11328]]; /* GLAGOLITIC CAPITAL LETTER NASHI */ +$config['2c00_2c5f'][] = ['upper' => 11281, 'status' => 'C', 'lower' => [11329]]; /* GLAGOLITIC CAPITAL LETTER ONU */ +$config['2c00_2c5f'][] = ['upper' => 11282, 'status' => 'C', 'lower' => [11330]]; /* GLAGOLITIC CAPITAL LETTER POKOJI */ +$config['2c00_2c5f'][] = ['upper' => 11283, 'status' => 'C', 'lower' => [11331]]; /* GLAGOLITIC CAPITAL LETTER RITSI */ +$config['2c00_2c5f'][] = ['upper' => 11284, 'status' => 'C', 'lower' => [11332]]; /* GLAGOLITIC CAPITAL LETTER SLOVO */ +$config['2c00_2c5f'][] = ['upper' => 11285, 'status' => 'C', 'lower' => [11333]]; /* GLAGOLITIC CAPITAL LETTER TVRIDO */ +$config['2c00_2c5f'][] = ['upper' => 11286, 'status' => 'C', 'lower' => [11334]]; /* GLAGOLITIC CAPITAL LETTER UKU */ +$config['2c00_2c5f'][] = ['upper' => 11287, 'status' => 'C', 'lower' => [11335]]; /* GLAGOLITIC CAPITAL LETTER FRITU */ +$config['2c00_2c5f'][] = ['upper' => 11288, 'status' => 'C', 'lower' => [11336]]; /* GLAGOLITIC CAPITAL LETTER HERU */ +$config['2c00_2c5f'][] = ['upper' => 11289, 'status' => 'C', 'lower' => [11337]]; /* GLAGOLITIC CAPITAL LETTER OTU */ +$config['2c00_2c5f'][] = ['upper' => 11290, 'status' => 'C', 'lower' => [11338]]; /* GLAGOLITIC CAPITAL LETTER PE */ +$config['2c00_2c5f'][] = ['upper' => 11291, 'status' => 'C', 'lower' => [11339]]; /* GLAGOLITIC CAPITAL LETTER SHTA */ +$config['2c00_2c5f'][] = ['upper' => 11292, 'status' => 'C', 'lower' => [11340]]; /* GLAGOLITIC CAPITAL LETTER TSI */ +$config['2c00_2c5f'][] = ['upper' => 11293, 'status' => 'C', 'lower' => [11341]]; /* GLAGOLITIC CAPITAL LETTER CHRIVI */ +$config['2c00_2c5f'][] = ['upper' => 11294, 'status' => 'C', 'lower' => [11342]]; /* GLAGOLITIC CAPITAL LETTER SHA */ +$config['2c00_2c5f'][] = ['upper' => 11295, 'status' => 'C', 'lower' => [11343]]; /* GLAGOLITIC CAPITAL LETTER YERU */ +$config['2c00_2c5f'][] = ['upper' => 11296, 'status' => 'C', 'lower' => [11344]]; /* GLAGOLITIC CAPITAL LETTER YERI */ +$config['2c00_2c5f'][] = ['upper' => 11297, 'status' => 'C', 'lower' => [11345]]; /* GLAGOLITIC CAPITAL LETTER YATI */ +$config['2c00_2c5f'][] = ['upper' => 11298, 'status' => 'C', 'lower' => [11346]]; /* GLAGOLITIC CAPITAL LETTER SPIDERY HA */ +$config['2c00_2c5f'][] = ['upper' => 11299, 'status' => 'C', 'lower' => [11347]]; /* GLAGOLITIC CAPITAL LETTER YU */ +$config['2c00_2c5f'][] = ['upper' => 11300, 'status' => 'C', 'lower' => [11348]]; /* GLAGOLITIC CAPITAL LETTER SMALL YUS */ +$config['2c00_2c5f'][] = ['upper' => 11301, 'status' => 'C', 'lower' => [11349]]; /* GLAGOLITIC CAPITAL LETTER SMALL YUS WITH TAIL */ +$config['2c00_2c5f'][] = ['upper' => 11302, 'status' => 'C', 'lower' => [11350]]; /* GLAGOLITIC CAPITAL LETTER YO */ +$config['2c00_2c5f'][] = ['upper' => 11303, 'status' => 'C', 'lower' => [11351]]; /* GLAGOLITIC CAPITAL LETTER IOTATED SMALL YUS */ +$config['2c00_2c5f'][] = ['upper' => 11304, 'status' => 'C', 'lower' => [11352]]; /* GLAGOLITIC CAPITAL LETTER BIG YUS */ +$config['2c00_2c5f'][] = ['upper' => 11305, 'status' => 'C', 'lower' => [11353]]; /* GLAGOLITIC CAPITAL LETTER IOTATED BIG YUS */ +$config['2c00_2c5f'][] = ['upper' => 11306, 'status' => 'C', 'lower' => [11354]]; /* GLAGOLITIC CAPITAL LETTER FITA */ +$config['2c00_2c5f'][] = ['upper' => 11307, 'status' => 'C', 'lower' => [11355]]; /* GLAGOLITIC CAPITAL LETTER IZHITSA */ +$config['2c00_2c5f'][] = ['upper' => 11308, 'status' => 'C', 'lower' => [11356]]; /* GLAGOLITIC CAPITAL LETTER SHTAPIC */ +$config['2c00_2c5f'][] = ['upper' => 11309, 'status' => 'C', 'lower' => [11357]]; /* GLAGOLITIC CAPITAL LETTER TROKUTASTI A */ +$config['2c00_2c5f'][] = ['upper' => 11310, 'status' => 'C', 'lower' => [11358]]; /* GLAGOLITIC CAPITAL LETTER LATINATE MYSLITE */ diff --git a/lib/Cake/Config/unicode/casefolding/2c60_2c7f.php b/lib/Cake/Config/unicode/casefolding/2c60_2c7f.php index 57f82927..379e7731 100755 --- a/lib/Cake/Config/unicode/casefolding/2c60_2c7f.php +++ b/lib/Cake/Config/unicode/casefolding/2c60_2c7f.php @@ -27,7 +27,7 @@ * * The lower filed is an array of the decimal values that form the lower case version of a character. * - * The status field is: + * The status field is: * C: common case folding, common mappings shared by both simple and full mappings. * F: full case folding, mappings that cause strings to grow in length. Multiple characters are separated by spaces. * S: simple case folding, mappings to single characters where different from F. @@ -37,11 +37,11 @@ * Note that the Turkic mappings do not maintain canonical equivalence without additional processing. * See the discussions of case mapping in the Unicode Standard for more information. */ -$config['2c60_2c7f'][] = array('upper' => 11360, 'status' => 'C', 'lower' => array(11361)); /* LATIN CAPITAL LETTER L WITH DOUBLE BAR */ -$config['2c60_2c7f'][] = array('upper' => 11362, 'status' => 'C', 'lower' => array(619)); /* LATIN CAPITAL LETTER L WITH MIDDLE TILDE */ -$config['2c60_2c7f'][] = array('upper' => 11363, 'status' => 'C', 'lower' => array(7549)); /* LATIN CAPITAL LETTER P WITH STROKE */ -$config['2c60_2c7f'][] = array('upper' => 11364, 'status' => 'C', 'lower' => array(637)); /* LATIN CAPITAL LETTER R WITH TAIL */ -$config['2c60_2c7f'][] = array('upper' => 11367, 'status' => 'C', 'lower' => array(11368)); /* LATIN CAPITAL LETTER H WITH DESCENDER */ -$config['2c60_2c7f'][] = array('upper' => 11369, 'status' => 'C', 'lower' => array(11370)); /* LATIN CAPITAL LETTER K WITH DESCENDER */ -$config['2c60_2c7f'][] = array('upper' => 11371, 'status' => 'C', 'lower' => array(11372)); /* LATIN CAPITAL LETTER Z WITH DESCENDER */ -$config['2c60_2c7f'][] = array('upper' => 11381, 'status' => 'C', 'lower' => array(11382)); /* LATIN CAPITAL LETTER HALF H */ +$config['2c60_2c7f'][] = ['upper' => 11360, 'status' => 'C', 'lower' => [11361]]; /* LATIN CAPITAL LETTER L WITH DOUBLE BAR */ +$config['2c60_2c7f'][] = ['upper' => 11362, 'status' => 'C', 'lower' => [619]]; /* LATIN CAPITAL LETTER L WITH MIDDLE TILDE */ +$config['2c60_2c7f'][] = ['upper' => 11363, 'status' => 'C', 'lower' => [7549]]; /* LATIN CAPITAL LETTER P WITH STROKE */ +$config['2c60_2c7f'][] = ['upper' => 11364, 'status' => 'C', 'lower' => [637]]; /* LATIN CAPITAL LETTER R WITH TAIL */ +$config['2c60_2c7f'][] = ['upper' => 11367, 'status' => 'C', 'lower' => [11368]]; /* LATIN CAPITAL LETTER H WITH DESCENDER */ +$config['2c60_2c7f'][] = ['upper' => 11369, 'status' => 'C', 'lower' => [11370]]; /* LATIN CAPITAL LETTER K WITH DESCENDER */ +$config['2c60_2c7f'][] = ['upper' => 11371, 'status' => 'C', 'lower' => [11372]]; /* LATIN CAPITAL LETTER Z WITH DESCENDER */ +$config['2c60_2c7f'][] = ['upper' => 11381, 'status' => 'C', 'lower' => [11382]]; /* LATIN CAPITAL LETTER HALF H */ diff --git a/lib/Cake/Config/unicode/casefolding/2c80_2cff.php b/lib/Cake/Config/unicode/casefolding/2c80_2cff.php index 35766fa4..6f95c380 100755 --- a/lib/Cake/Config/unicode/casefolding/2c80_2cff.php +++ b/lib/Cake/Config/unicode/casefolding/2c80_2cff.php @@ -27,7 +27,7 @@ * * The lower filed is an array of the decimal values that form the lower case version of a character. * - * The status field is: + * The status field is: * C: common case folding, common mappings shared by both simple and full mappings. * F: full case folding, mappings that cause strings to grow in length. Multiple characters are separated by spaces. * S: simple case folding, mappings to single characters where different from F. @@ -37,53 +37,53 @@ * Note that the Turkic mappings do not maintain canonical equivalence without additional processing. * See the discussions of case mapping in the Unicode Standard for more information. */ -$config['2c80_2cff'][] = array('upper' => 11392, 'status' => 'C', 'lower' => array(11393)); /* COPTIC CAPITAL LETTER ALFA */ -$config['2c80_2cff'][] = array('upper' => 11394, 'status' => 'C', 'lower' => array(11395)); /* COPTIC CAPITAL LETTER VIDA */ -$config['2c80_2cff'][] = array('upper' => 11396, 'status' => 'C', 'lower' => array(11397)); /* COPTIC CAPITAL LETTER GAMMA */ -$config['2c80_2cff'][] = array('upper' => 11398, 'status' => 'C', 'lower' => array(11399)); /* COPTIC CAPITAL LETTER DALDA */ -$config['2c80_2cff'][] = array('upper' => 11400, 'status' => 'C', 'lower' => array(11401)); /* COPTIC CAPITAL LETTER EIE */ -$config['2c80_2cff'][] = array('upper' => 11402, 'status' => 'C', 'lower' => array(11403)); /* COPTIC CAPITAL LETTER SOU */ -$config['2c80_2cff'][] = array('upper' => 11404, 'status' => 'C', 'lower' => array(11405)); /* COPTIC CAPITAL LETTER ZATA */ -$config['2c80_2cff'][] = array('upper' => 11406, 'status' => 'C', 'lower' => array(11407)); /* COPTIC CAPITAL LETTER HATE */ -$config['2c80_2cff'][] = array('upper' => 11408, 'status' => 'C', 'lower' => array(11409)); /* COPTIC CAPITAL LETTER THETHE */ -$config['2c80_2cff'][] = array('upper' => 11410, 'status' => 'C', 'lower' => array(11411)); /* COPTIC CAPITAL LETTER IAUDA */ -$config['2c80_2cff'][] = array('upper' => 11412, 'status' => 'C', 'lower' => array(11413)); /* COPTIC CAPITAL LETTER KAPA */ -$config['2c80_2cff'][] = array('upper' => 11414, 'status' => 'C', 'lower' => array(11415)); /* COPTIC CAPITAL LETTER LAULA */ -$config['2c80_2cff'][] = array('upper' => 11416, 'status' => 'C', 'lower' => array(11417)); /* COPTIC CAPITAL LETTER MI */ -$config['2c80_2cff'][] = array('upper' => 11418, 'status' => 'C', 'lower' => array(11419)); /* COPTIC CAPITAL LETTER NI */ -$config['2c80_2cff'][] = array('upper' => 11420, 'status' => 'C', 'lower' => array(11421)); /* COPTIC CAPITAL LETTER KSI */ -$config['2c80_2cff'][] = array('upper' => 11422, 'status' => 'C', 'lower' => array(11423)); /* COPTIC CAPITAL LETTER O */ -$config['2c80_2cff'][] = array('upper' => 11424, 'status' => 'C', 'lower' => array(11425)); /* COPTIC CAPITAL LETTER PI */ -$config['2c80_2cff'][] = array('upper' => 11426, 'status' => 'C', 'lower' => array(11427)); /* COPTIC CAPITAL LETTER RO */ -$config['2c80_2cff'][] = array('upper' => 11428, 'status' => 'C', 'lower' => array(11429)); /* COPTIC CAPITAL LETTER SIMA */ -$config['2c80_2cff'][] = array('upper' => 11430, 'status' => 'C', 'lower' => array(11431)); /* COPTIC CAPITAL LETTER TAU */ -$config['2c80_2cff'][] = array('upper' => 11432, 'status' => 'C', 'lower' => array(11433)); /* COPTIC CAPITAL LETTER UA */ -$config['2c80_2cff'][] = array('upper' => 11434, 'status' => 'C', 'lower' => array(11435)); /* COPTIC CAPITAL LETTER FI */ -$config['2c80_2cff'][] = array('upper' => 11436, 'status' => 'C', 'lower' => array(11437)); /* COPTIC CAPITAL LETTER KHI */ -$config['2c80_2cff'][] = array('upper' => 11438, 'status' => 'C', 'lower' => array(11439)); /* COPTIC CAPITAL LETTER PSI */ -$config['2c80_2cff'][] = array('upper' => 11440, 'status' => 'C', 'lower' => array(11441)); /* COPTIC CAPITAL LETTER OOU */ -$config['2c80_2cff'][] = array('upper' => 11442, 'status' => 'C', 'lower' => array(11443)); /* COPTIC CAPITAL LETTER DIALECT-P ALEF */ -$config['2c80_2cff'][] = array('upper' => 11444, 'status' => 'C', 'lower' => array(11445)); /* COPTIC CAPITAL LETTER OLD COPTIC AIN */ -$config['2c80_2cff'][] = array('upper' => 11446, 'status' => 'C', 'lower' => array(11447)); /* COPTIC CAPITAL LETTER CRYPTOGRAMMIC EIE */ -$config['2c80_2cff'][] = array('upper' => 11448, 'status' => 'C', 'lower' => array(11449)); /* COPTIC CAPITAL LETTER DIALECT-P KAPA */ -$config['2c80_2cff'][] = array('upper' => 11450, 'status' => 'C', 'lower' => array(11451)); /* COPTIC CAPITAL LETTER DIALECT-P NI */ -$config['2c80_2cff'][] = array('upper' => 11452, 'status' => 'C', 'lower' => array(11453)); /* COPTIC CAPITAL LETTER CRYPTOGRAMMIC NI */ -$config['2c80_2cff'][] = array('upper' => 11454, 'status' => 'C', 'lower' => array(11455)); /* COPTIC CAPITAL LETTER OLD COPTIC OOU */ -$config['2c80_2cff'][] = array('upper' => 11456, 'status' => 'C', 'lower' => array(11457)); /* COPTIC CAPITAL LETTER SAMPI */ -$config['2c80_2cff'][] = array('upper' => 11458, 'status' => 'C', 'lower' => array(11459)); /* COPTIC CAPITAL LETTER CROSSED SHEI */ -$config['2c80_2cff'][] = array('upper' => 11460, 'status' => 'C', 'lower' => array(11461)); /* COPTIC CAPITAL LETTER OLD COPTIC SHEI */ -$config['2c80_2cff'][] = array('upper' => 11462, 'status' => 'C', 'lower' => array(11463)); /* COPTIC CAPITAL LETTER OLD COPTIC ESH */ -$config['2c80_2cff'][] = array('upper' => 11464, 'status' => 'C', 'lower' => array(11465)); /* COPTIC CAPITAL LETTER AKHMIMIC KHEI */ -$config['2c80_2cff'][] = array('upper' => 11466, 'status' => 'C', 'lower' => array(11467)); /* COPTIC CAPITAL LETTER DIALECT-P HORI */ -$config['2c80_2cff'][] = array('upper' => 11468, 'status' => 'C', 'lower' => array(11469)); /* COPTIC CAPITAL LETTER OLD COPTIC HORI */ -$config['2c80_2cff'][] = array('upper' => 11470, 'status' => 'C', 'lower' => array(11471)); /* COPTIC CAPITAL LETTER OLD COPTIC HA */ -$config['2c80_2cff'][] = array('upper' => 11472, 'status' => 'C', 'lower' => array(11473)); /* COPTIC CAPITAL LETTER L-SHAPED HA */ -$config['2c80_2cff'][] = array('upper' => 11474, 'status' => 'C', 'lower' => array(11475)); /* COPTIC CAPITAL LETTER OLD COPTIC HEI */ -$config['2c80_2cff'][] = array('upper' => 11476, 'status' => 'C', 'lower' => array(11477)); /* COPTIC CAPITAL LETTER OLD COPTIC HAT */ -$config['2c80_2cff'][] = array('upper' => 11478, 'status' => 'C', 'lower' => array(11479)); /* COPTIC CAPITAL LETTER OLD COPTIC GANGIA */ -$config['2c80_2cff'][] = array('upper' => 11480, 'status' => 'C', 'lower' => array(11481)); /* COPTIC CAPITAL LETTER OLD COPTIC DJA */ -$config['2c80_2cff'][] = array('upper' => 11482, 'status' => 'C', 'lower' => array(11483)); /* COPTIC CAPITAL LETTER OLD COPTIC SHIMA */ -$config['2c80_2cff'][] = array('upper' => 11484, 'status' => 'C', 'lower' => array(11485)); /* COPTIC CAPITAL LETTER OLD NUBIAN SHIMA */ -$config['2c80_2cff'][] = array('upper' => 11486, 'status' => 'C', 'lower' => array(11487)); /* COPTIC CAPITAL LETTER OLD NUBIAN NGI */ -$config['2c80_2cff'][] = array('upper' => 11488, 'status' => 'C', 'lower' => array(11489)); /* COPTIC CAPITAL LETTER OLD NUBIAN NYI */ -$config['2c80_2cff'][] = array('upper' => 11490, 'status' => 'C', 'lower' => array(11491)); /* COPTIC CAPITAL LETTER OLD NUBIAN WAU */ +$config['2c80_2cff'][] = ['upper' => 11392, 'status' => 'C', 'lower' => [11393]]; /* COPTIC CAPITAL LETTER ALFA */ +$config['2c80_2cff'][] = ['upper' => 11394, 'status' => 'C', 'lower' => [11395]]; /* COPTIC CAPITAL LETTER VIDA */ +$config['2c80_2cff'][] = ['upper' => 11396, 'status' => 'C', 'lower' => [11397]]; /* COPTIC CAPITAL LETTER GAMMA */ +$config['2c80_2cff'][] = ['upper' => 11398, 'status' => 'C', 'lower' => [11399]]; /* COPTIC CAPITAL LETTER DALDA */ +$config['2c80_2cff'][] = ['upper' => 11400, 'status' => 'C', 'lower' => [11401]]; /* COPTIC CAPITAL LETTER EIE */ +$config['2c80_2cff'][] = ['upper' => 11402, 'status' => 'C', 'lower' => [11403]]; /* COPTIC CAPITAL LETTER SOU */ +$config['2c80_2cff'][] = ['upper' => 11404, 'status' => 'C', 'lower' => [11405]]; /* COPTIC CAPITAL LETTER ZATA */ +$config['2c80_2cff'][] = ['upper' => 11406, 'status' => 'C', 'lower' => [11407]]; /* COPTIC CAPITAL LETTER HATE */ +$config['2c80_2cff'][] = ['upper' => 11408, 'status' => 'C', 'lower' => [11409]]; /* COPTIC CAPITAL LETTER THETHE */ +$config['2c80_2cff'][] = ['upper' => 11410, 'status' => 'C', 'lower' => [11411]]; /* COPTIC CAPITAL LETTER IAUDA */ +$config['2c80_2cff'][] = ['upper' => 11412, 'status' => 'C', 'lower' => [11413]]; /* COPTIC CAPITAL LETTER KAPA */ +$config['2c80_2cff'][] = ['upper' => 11414, 'status' => 'C', 'lower' => [11415]]; /* COPTIC CAPITAL LETTER LAULA */ +$config['2c80_2cff'][] = ['upper' => 11416, 'status' => 'C', 'lower' => [11417]]; /* COPTIC CAPITAL LETTER MI */ +$config['2c80_2cff'][] = ['upper' => 11418, 'status' => 'C', 'lower' => [11419]]; /* COPTIC CAPITAL LETTER NI */ +$config['2c80_2cff'][] = ['upper' => 11420, 'status' => 'C', 'lower' => [11421]]; /* COPTIC CAPITAL LETTER KSI */ +$config['2c80_2cff'][] = ['upper' => 11422, 'status' => 'C', 'lower' => [11423]]; /* COPTIC CAPITAL LETTER O */ +$config['2c80_2cff'][] = ['upper' => 11424, 'status' => 'C', 'lower' => [11425]]; /* COPTIC CAPITAL LETTER PI */ +$config['2c80_2cff'][] = ['upper' => 11426, 'status' => 'C', 'lower' => [11427]]; /* COPTIC CAPITAL LETTER RO */ +$config['2c80_2cff'][] = ['upper' => 11428, 'status' => 'C', 'lower' => [11429]]; /* COPTIC CAPITAL LETTER SIMA */ +$config['2c80_2cff'][] = ['upper' => 11430, 'status' => 'C', 'lower' => [11431]]; /* COPTIC CAPITAL LETTER TAU */ +$config['2c80_2cff'][] = ['upper' => 11432, 'status' => 'C', 'lower' => [11433]]; /* COPTIC CAPITAL LETTER UA */ +$config['2c80_2cff'][] = ['upper' => 11434, 'status' => 'C', 'lower' => [11435]]; /* COPTIC CAPITAL LETTER FI */ +$config['2c80_2cff'][] = ['upper' => 11436, 'status' => 'C', 'lower' => [11437]]; /* COPTIC CAPITAL LETTER KHI */ +$config['2c80_2cff'][] = ['upper' => 11438, 'status' => 'C', 'lower' => [11439]]; /* COPTIC CAPITAL LETTER PSI */ +$config['2c80_2cff'][] = ['upper' => 11440, 'status' => 'C', 'lower' => [11441]]; /* COPTIC CAPITAL LETTER OOU */ +$config['2c80_2cff'][] = ['upper' => 11442, 'status' => 'C', 'lower' => [11443]]; /* COPTIC CAPITAL LETTER DIALECT-P ALEF */ +$config['2c80_2cff'][] = ['upper' => 11444, 'status' => 'C', 'lower' => [11445]]; /* COPTIC CAPITAL LETTER OLD COPTIC AIN */ +$config['2c80_2cff'][] = ['upper' => 11446, 'status' => 'C', 'lower' => [11447]]; /* COPTIC CAPITAL LETTER CRYPTOGRAMMIC EIE */ +$config['2c80_2cff'][] = ['upper' => 11448, 'status' => 'C', 'lower' => [11449]]; /* COPTIC CAPITAL LETTER DIALECT-P KAPA */ +$config['2c80_2cff'][] = ['upper' => 11450, 'status' => 'C', 'lower' => [11451]]; /* COPTIC CAPITAL LETTER DIALECT-P NI */ +$config['2c80_2cff'][] = ['upper' => 11452, 'status' => 'C', 'lower' => [11453]]; /* COPTIC CAPITAL LETTER CRYPTOGRAMMIC NI */ +$config['2c80_2cff'][] = ['upper' => 11454, 'status' => 'C', 'lower' => [11455]]; /* COPTIC CAPITAL LETTER OLD COPTIC OOU */ +$config['2c80_2cff'][] = ['upper' => 11456, 'status' => 'C', 'lower' => [11457]]; /* COPTIC CAPITAL LETTER SAMPI */ +$config['2c80_2cff'][] = ['upper' => 11458, 'status' => 'C', 'lower' => [11459]]; /* COPTIC CAPITAL LETTER CROSSED SHEI */ +$config['2c80_2cff'][] = ['upper' => 11460, 'status' => 'C', 'lower' => [11461]]; /* COPTIC CAPITAL LETTER OLD COPTIC SHEI */ +$config['2c80_2cff'][] = ['upper' => 11462, 'status' => 'C', 'lower' => [11463]]; /* COPTIC CAPITAL LETTER OLD COPTIC ESH */ +$config['2c80_2cff'][] = ['upper' => 11464, 'status' => 'C', 'lower' => [11465]]; /* COPTIC CAPITAL LETTER AKHMIMIC KHEI */ +$config['2c80_2cff'][] = ['upper' => 11466, 'status' => 'C', 'lower' => [11467]]; /* COPTIC CAPITAL LETTER DIALECT-P HORI */ +$config['2c80_2cff'][] = ['upper' => 11468, 'status' => 'C', 'lower' => [11469]]; /* COPTIC CAPITAL LETTER OLD COPTIC HORI */ +$config['2c80_2cff'][] = ['upper' => 11470, 'status' => 'C', 'lower' => [11471]]; /* COPTIC CAPITAL LETTER OLD COPTIC HA */ +$config['2c80_2cff'][] = ['upper' => 11472, 'status' => 'C', 'lower' => [11473]]; /* COPTIC CAPITAL LETTER L-SHAPED HA */ +$config['2c80_2cff'][] = ['upper' => 11474, 'status' => 'C', 'lower' => [11475]]; /* COPTIC CAPITAL LETTER OLD COPTIC HEI */ +$config['2c80_2cff'][] = ['upper' => 11476, 'status' => 'C', 'lower' => [11477]]; /* COPTIC CAPITAL LETTER OLD COPTIC HAT */ +$config['2c80_2cff'][] = ['upper' => 11478, 'status' => 'C', 'lower' => [11479]]; /* COPTIC CAPITAL LETTER OLD COPTIC GANGIA */ +$config['2c80_2cff'][] = ['upper' => 11480, 'status' => 'C', 'lower' => [11481]]; /* COPTIC CAPITAL LETTER OLD COPTIC DJA */ +$config['2c80_2cff'][] = ['upper' => 11482, 'status' => 'C', 'lower' => [11483]]; /* COPTIC CAPITAL LETTER OLD COPTIC SHIMA */ +$config['2c80_2cff'][] = ['upper' => 11484, 'status' => 'C', 'lower' => [11485]]; /* COPTIC CAPITAL LETTER OLD NUBIAN SHIMA */ +$config['2c80_2cff'][] = ['upper' => 11486, 'status' => 'C', 'lower' => [11487]]; /* COPTIC CAPITAL LETTER OLD NUBIAN NGI */ +$config['2c80_2cff'][] = ['upper' => 11488, 'status' => 'C', 'lower' => [11489]]; /* COPTIC CAPITAL LETTER OLD NUBIAN NYI */ +$config['2c80_2cff'][] = ['upper' => 11490, 'status' => 'C', 'lower' => [11491]]; /* COPTIC CAPITAL LETTER OLD NUBIAN WAU */ diff --git a/lib/Cake/Config/unicode/casefolding/ff00_ffef.php b/lib/Cake/Config/unicode/casefolding/ff00_ffef.php index 9bf25e37..cb0c5179 100755 --- a/lib/Cake/Config/unicode/casefolding/ff00_ffef.php +++ b/lib/Cake/Config/unicode/casefolding/ff00_ffef.php @@ -27,7 +27,7 @@ * * The lower filed is an array of the decimal values that form the lower case version of a character. * - * The status field is: + * The status field is: * C: common case folding, common mappings shared by both simple and full mappings. * F: full case folding, mappings that cause strings to grow in length. Multiple characters are separated by spaces. * S: simple case folding, mappings to single characters where different from F. @@ -37,29 +37,29 @@ * Note that the Turkic mappings do not maintain canonical equivalence without additional processing. * See the discussions of case mapping in the Unicode Standard for more information. */ -$config['ff00_ffef'][] = array('upper' => 65313, 'status' => 'C', 'lower' => array(65345)); /* FULLWIDTH LATIN CAPITAL LETTER A */ -$config['ff00_ffef'][] = array('upper' => 65314, 'status' => 'C', 'lower' => array(65346)); /* FULLWIDTH LATIN CAPITAL LETTER B */ -$config['ff00_ffef'][] = array('upper' => 65315, 'status' => 'C', 'lower' => array(65347)); /* FULLWIDTH LATIN CAPITAL LETTER C */ -$config['ff00_ffef'][] = array('upper' => 65316, 'status' => 'C', 'lower' => array(65348)); /* FULLWIDTH LATIN CAPITAL LETTER D */ -$config['ff00_ffef'][] = array('upper' => 65317, 'status' => 'C', 'lower' => array(65349)); /* FULLWIDTH LATIN CAPITAL LETTER E */ -$config['ff00_ffef'][] = array('upper' => 65318, 'status' => 'C', 'lower' => array(65350)); /* FULLWIDTH LATIN CAPITAL LETTER F */ -$config['ff00_ffef'][] = array('upper' => 65319, 'status' => 'C', 'lower' => array(65351)); /* FULLWIDTH LATIN CAPITAL LETTER G */ -$config['ff00_ffef'][] = array('upper' => 65320, 'status' => 'C', 'lower' => array(65352)); /* FULLWIDTH LATIN CAPITAL LETTER H */ -$config['ff00_ffef'][] = array('upper' => 65321, 'status' => 'C', 'lower' => array(65353)); /* FULLWIDTH LATIN CAPITAL LETTER I */ -$config['ff00_ffef'][] = array('upper' => 65322, 'status' => 'C', 'lower' => array(65354)); /* FULLWIDTH LATIN CAPITAL LETTER J */ -$config['ff00_ffef'][] = array('upper' => 65323, 'status' => 'C', 'lower' => array(65355)); /* FULLWIDTH LATIN CAPITAL LETTER K */ -$config['ff00_ffef'][] = array('upper' => 65324, 'status' => 'C', 'lower' => array(65356)); /* FULLWIDTH LATIN CAPITAL LETTER L */ -$config['ff00_ffef'][] = array('upper' => 65325, 'status' => 'C', 'lower' => array(65357)); /* FULLWIDTH LATIN CAPITAL LETTER M */ -$config['ff00_ffef'][] = array('upper' => 65326, 'status' => 'C', 'lower' => array(65358)); /* FULLWIDTH LATIN CAPITAL LETTER N */ -$config['ff00_ffef'][] = array('upper' => 65327, 'status' => 'C', 'lower' => array(65359)); /* FULLWIDTH LATIN CAPITAL LETTER O */ -$config['ff00_ffef'][] = array('upper' => 65328, 'status' => 'C', 'lower' => array(65360)); /* FULLWIDTH LATIN CAPITAL LETTER P */ -$config['ff00_ffef'][] = array('upper' => 65329, 'status' => 'C', 'lower' => array(65361)); /* FULLWIDTH LATIN CAPITAL LETTER Q */ -$config['ff00_ffef'][] = array('upper' => 65330, 'status' => 'C', 'lower' => array(65362)); /* FULLWIDTH LATIN CAPITAL LETTER R */ -$config['ff00_ffef'][] = array('upper' => 65331, 'status' => 'C', 'lower' => array(65363)); /* FULLWIDTH LATIN CAPITAL LETTER S */ -$config['ff00_ffef'][] = array('upper' => 65332, 'status' => 'C', 'lower' => array(65364)); /* FULLWIDTH LATIN CAPITAL LETTER T */ -$config['ff00_ffef'][] = array('upper' => 65333, 'status' => 'C', 'lower' => array(65365)); /* FULLWIDTH LATIN CAPITAL LETTER U */ -$config['ff00_ffef'][] = array('upper' => 65334, 'status' => 'C', 'lower' => array(65366)); /* FULLWIDTH LATIN CAPITAL LETTER V */ -$config['ff00_ffef'][] = array('upper' => 65335, 'status' => 'C', 'lower' => array(65367)); /* FULLWIDTH LATIN CAPITAL LETTER W */ -$config['ff00_ffef'][] = array('upper' => 65336, 'status' => 'C', 'lower' => array(65368)); /* FULLWIDTH LATIN CAPITAL LETTER X */ -$config['ff00_ffef'][] = array('upper' => 65337, 'status' => 'C', 'lower' => array(65369)); /* FULLWIDTH LATIN CAPITAL LETTER Y */ -$config['ff00_ffef'][] = array('upper' => 65338, 'status' => 'C', 'lower' => array(65370)); /* FULLWIDTH LATIN CAPITAL LETTER Z */ +$config['ff00_ffef'][] = ['upper' => 65313, 'status' => 'C', 'lower' => [65345]]; /* FULLWIDTH LATIN CAPITAL LETTER A */ +$config['ff00_ffef'][] = ['upper' => 65314, 'status' => 'C', 'lower' => [65346]]; /* FULLWIDTH LATIN CAPITAL LETTER B */ +$config['ff00_ffef'][] = ['upper' => 65315, 'status' => 'C', 'lower' => [65347]]; /* FULLWIDTH LATIN CAPITAL LETTER C */ +$config['ff00_ffef'][] = ['upper' => 65316, 'status' => 'C', 'lower' => [65348]]; /* FULLWIDTH LATIN CAPITAL LETTER D */ +$config['ff00_ffef'][] = ['upper' => 65317, 'status' => 'C', 'lower' => [65349]]; /* FULLWIDTH LATIN CAPITAL LETTER E */ +$config['ff00_ffef'][] = ['upper' => 65318, 'status' => 'C', 'lower' => [65350]]; /* FULLWIDTH LATIN CAPITAL LETTER F */ +$config['ff00_ffef'][] = ['upper' => 65319, 'status' => 'C', 'lower' => [65351]]; /* FULLWIDTH LATIN CAPITAL LETTER G */ +$config['ff00_ffef'][] = ['upper' => 65320, 'status' => 'C', 'lower' => [65352]]; /* FULLWIDTH LATIN CAPITAL LETTER H */ +$config['ff00_ffef'][] = ['upper' => 65321, 'status' => 'C', 'lower' => [65353]]; /* FULLWIDTH LATIN CAPITAL LETTER I */ +$config['ff00_ffef'][] = ['upper' => 65322, 'status' => 'C', 'lower' => [65354]]; /* FULLWIDTH LATIN CAPITAL LETTER J */ +$config['ff00_ffef'][] = ['upper' => 65323, 'status' => 'C', 'lower' => [65355]]; /* FULLWIDTH LATIN CAPITAL LETTER K */ +$config['ff00_ffef'][] = ['upper' => 65324, 'status' => 'C', 'lower' => [65356]]; /* FULLWIDTH LATIN CAPITAL LETTER L */ +$config['ff00_ffef'][] = ['upper' => 65325, 'status' => 'C', 'lower' => [65357]]; /* FULLWIDTH LATIN CAPITAL LETTER M */ +$config['ff00_ffef'][] = ['upper' => 65326, 'status' => 'C', 'lower' => [65358]]; /* FULLWIDTH LATIN CAPITAL LETTER N */ +$config['ff00_ffef'][] = ['upper' => 65327, 'status' => 'C', 'lower' => [65359]]; /* FULLWIDTH LATIN CAPITAL LETTER O */ +$config['ff00_ffef'][] = ['upper' => 65328, 'status' => 'C', 'lower' => [65360]]; /* FULLWIDTH LATIN CAPITAL LETTER P */ +$config['ff00_ffef'][] = ['upper' => 65329, 'status' => 'C', 'lower' => [65361]]; /* FULLWIDTH LATIN CAPITAL LETTER Q */ +$config['ff00_ffef'][] = ['upper' => 65330, 'status' => 'C', 'lower' => [65362]]; /* FULLWIDTH LATIN CAPITAL LETTER R */ +$config['ff00_ffef'][] = ['upper' => 65331, 'status' => 'C', 'lower' => [65363]]; /* FULLWIDTH LATIN CAPITAL LETTER S */ +$config['ff00_ffef'][] = ['upper' => 65332, 'status' => 'C', 'lower' => [65364]]; /* FULLWIDTH LATIN CAPITAL LETTER T */ +$config['ff00_ffef'][] = ['upper' => 65333, 'status' => 'C', 'lower' => [65365]]; /* FULLWIDTH LATIN CAPITAL LETTER U */ +$config['ff00_ffef'][] = ['upper' => 65334, 'status' => 'C', 'lower' => [65366]]; /* FULLWIDTH LATIN CAPITAL LETTER V */ +$config['ff00_ffef'][] = ['upper' => 65335, 'status' => 'C', 'lower' => [65367]]; /* FULLWIDTH LATIN CAPITAL LETTER W */ +$config['ff00_ffef'][] = ['upper' => 65336, 'status' => 'C', 'lower' => [65368]]; /* FULLWIDTH LATIN CAPITAL LETTER X */ +$config['ff00_ffef'][] = ['upper' => 65337, 'status' => 'C', 'lower' => [65369]]; /* FULLWIDTH LATIN CAPITAL LETTER Y */ +$config['ff00_ffef'][] = ['upper' => 65338, 'status' => 'C', 'lower' => [65370]]; /* FULLWIDTH LATIN CAPITAL LETTER Z */ diff --git a/lib/Cake/Configure/ConfigReaderInterface.php b/lib/Cake/Configure/ConfigReaderInterface.php index 32e0e61c..d5c98772 100755 --- a/lib/Cake/Configure/ConfigReaderInterface.php +++ b/lib/Cake/Configure/ConfigReaderInterface.php @@ -19,25 +19,26 @@ * * @package Cake.Core */ -interface ConfigReaderInterface { +interface ConfigReaderInterface +{ -/** - * Read method is used for reading configuration information from sources. - * These sources can either be static resources like files, or dynamic ones like - * a database, or other datasource. - * - * @param string $key Key to read. - * @return array An array of data to merge into the runtime configuration - */ - public function read($key); + /** + * Read method is used for reading configuration information from sources. + * These sources can either be static resources like files, or dynamic ones like + * a database, or other datasource. + * + * @param string $key Key to read. + * @return array An array of data to merge into the runtime configuration + */ + public function read($key); -/** - * Dumps the configure data into source. - * - * @param string $key The identifier to write to. - * @param array $data The data to dump. - * @return bool True on success or false on failure. - */ - public function dump($key, $data); + /** + * Dumps the configure data into source. + * + * @param string $key The identifier to write to. + * @param array $data The data to dump. + * @return bool True on success or false on failure. + */ + public function dump($key, $data); } diff --git a/lib/Cake/Configure/IniReader.php b/lib/Cake/Configure/IniReader.php index 2c7bc763..0f89b7e5 100755 --- a/lib/Cake/Configure/IniReader.php +++ b/lib/Cake/Configure/IniReader.php @@ -53,178 +53,185 @@ * @package Cake.Configure * @see http://php.net/parse_ini_file */ -class IniReader implements ConfigReaderInterface { - -/** - * The path to read ini files from. - * - * @var array - */ - protected $_path; - -/** - * The section to read, if null all sections will be read. - * - * @var string - */ - protected $_section; - -/** - * Build and construct a new ini file parser. The parser can be used to read - * ini files that are on the filesystem. - * - * @param string $path Path to load ini config files from. Defaults to CONFIG - * @param string $section Only get one section, leave null to parse and fetch - * all sections in the ini file. - */ - public function __construct($path = null, $section = null) { - if (!$path) { - $path = CONFIG; - } - $this->_path = $path; - $this->_section = $section; - } - -/** - * Read an ini file and return the results as an array. - * - * For backwards compatibility, acl.ini.php will be treated specially until 3.0. - * - * @param string $key The identifier to read from. If the key has a . it will be treated - * as a plugin prefix. The chosen file must be on the reader's path. - * @return array Parsed configuration values. - * @throws ConfigureException when files don't exist. - * Or when files contain '..' as this could lead to abusive reads. - */ - public function read($key) { - if (strpos($key, '..') !== false) { - throw new ConfigureException(__d('cake_dev', 'Cannot load configuration files with ../ in them.')); - } - - $file = $this->_getFilePath($key); - if (!is_file(realpath($file))) { - throw new ConfigureException(__d('cake_dev', 'Could not load configuration file: %s', $file)); - } - - $contents = parse_ini_file($file, true); - if (!empty($this->_section) && isset($contents[$this->_section])) { - $values = $this->_parseNestedValues($contents[$this->_section]); - } else { - $values = array(); - foreach ($contents as $section => $attribs) { - if (is_array($attribs)) { - $values[$section] = $this->_parseNestedValues($attribs); - } else { - $parse = $this->_parseNestedValues(array($attribs)); - $values[$section] = array_shift($parse); - } - } - } - return $values; - } - -/** - * parses nested values out of keys. - * - * @param array $values Values to be exploded. - * @return array Array of values exploded - */ - protected function _parseNestedValues($values) { - foreach ($values as $key => $value) { - if ($value === '1') { - $value = true; - } - if ($value === '') { - $value = false; - } - unset($values[$key]); - if (strpos($key, '.') !== false) { - $values = Hash::insert($values, $key, $value); - } else { - $values[$key] = $value; - } - } - return $values; - } - -/** - * Dumps the state of Configure data into an ini formatted string. - * - * @param string $key The identifier to write to. If the key has a . it will be treated - * as a plugin prefix. - * @param array $data The data to convert to ini file. - * @return int Bytes saved. - */ - public function dump($key, $data) { - $result = array(); - foreach ($data as $k => $value) { - $isSection = false; - if ($k[0] !== '[') { - $result[] = "[$k]"; - $isSection = true; - } - if (is_array($value)) { - $kValues = Hash::flatten($value, '.'); - foreach ($kValues as $k2 => $v) { - $result[] = "$k2 = " . $this->_value($v); - } - } - if ($isSection) { - $result[] = ''; - } - } - $contents = trim(implode("\n", $result)); - - $filename = $this->_getFilePath($key); - return file_put_contents($filename, $contents); - } - -/** - * Converts a value into the ini equivalent - * - * @param mixed $val Value to export. - * @return string String value for ini file. - */ - protected function _value($val) { - if ($val === null) { - return 'null'; - } - if ($val === true) { - return 'true'; - } - if ($val === false) { - return 'false'; - } - return (string)$val; - } - -/** - * Get file path - * - * @param string $key The identifier to write to. If the key has a . it will be treated - * as a plugin prefix. - * @return string Full file path - */ - protected function _getFilePath($key) { - if (substr($key, -8) === '.ini.php') { - $key = substr($key, 0, -8); - list($plugin, $key) = pluginSplit($key); - $key .= '.ini.php'; - } else { - if (substr($key, -4) === '.ini') { - $key = substr($key, 0, -4); - } - list($plugin, $key) = pluginSplit($key); - $key .= '.ini'; - } - - if ($plugin) { - $file = CakePlugin::path($plugin) . 'Config' . DS . $key; - } else { - $file = $this->_path . $key; - } - - return $file; - } +class IniReader implements ConfigReaderInterface +{ + + /** + * The path to read ini files from. + * + * @var array + */ + protected $_path; + + /** + * The section to read, if null all sections will be read. + * + * @var string + */ + protected $_section; + + /** + * Build and construct a new ini file parser. The parser can be used to read + * ini files that are on the filesystem. + * + * @param string $path Path to load ini config files from. Defaults to CONFIG + * @param string $section Only get one section, leave null to parse and fetch + * all sections in the ini file. + */ + public function __construct($path = null, $section = null) + { + if (!$path) { + $path = CONFIG; + } + $this->_path = $path; + $this->_section = $section; + } + + /** + * Read an ini file and return the results as an array. + * + * For backwards compatibility, acl.ini.php will be treated specially until 3.0. + * + * @param string $key The identifier to read from. If the key has a . it will be treated + * as a plugin prefix. The chosen file must be on the reader's path. + * @return array Parsed configuration values. + * @throws ConfigureException when files don't exist. + * Or when files contain '..' as this could lead to abusive reads. + */ + public function read($key) + { + if (strpos($key, '..') !== false) { + throw new ConfigureException(__d('cake_dev', 'Cannot load configuration files with ../ in them.')); + } + + $file = $this->_getFilePath($key); + if (!is_file(realpath($file))) { + throw new ConfigureException(__d('cake_dev', 'Could not load configuration file: %s', $file)); + } + + $contents = parse_ini_file($file, true); + if (!empty($this->_section) && isset($contents[$this->_section])) { + $values = $this->_parseNestedValues($contents[$this->_section]); + } else { + $values = []; + foreach ($contents as $section => $attribs) { + if (is_array($attribs)) { + $values[$section] = $this->_parseNestedValues($attribs); + } else { + $parse = $this->_parseNestedValues([$attribs]); + $values[$section] = array_shift($parse); + } + } + } + return $values; + } + + /** + * Get file path + * + * @param string $key The identifier to write to. If the key has a . it will be treated + * as a plugin prefix. + * @return string Full file path + */ + protected function _getFilePath($key) + { + if (substr($key, -8) === '.ini.php') { + $key = substr($key, 0, -8); + list($plugin, $key) = pluginSplit($key); + $key .= '.ini.php'; + } else { + if (substr($key, -4) === '.ini') { + $key = substr($key, 0, -4); + } + list($plugin, $key) = pluginSplit($key); + $key .= '.ini'; + } + + if ($plugin) { + $file = CakePlugin::path($plugin) . 'Config' . DS . $key; + } else { + $file = $this->_path . $key; + } + + return $file; + } + + /** + * parses nested values out of keys. + * + * @param array $values Values to be exploded. + * @return array Array of values exploded + */ + protected function _parseNestedValues($values) + { + foreach ($values as $key => $value) { + if ($value === '1') { + $value = true; + } + if ($value === '') { + $value = false; + } + unset($values[$key]); + if (strpos($key, '.') !== false) { + $values = Hash::insert($values, $key, $value); + } else { + $values[$key] = $value; + } + } + return $values; + } + + /** + * Dumps the state of Configure data into an ini formatted string. + * + * @param string $key The identifier to write to. If the key has a . it will be treated + * as a plugin prefix. + * @param array $data The data to convert to ini file. + * @return int Bytes saved. + */ + public function dump($key, $data) + { + $result = []; + foreach ($data as $k => $value) { + $isSection = false; + if ($k[0] !== '[') { + $result[] = "[$k]"; + $isSection = true; + } + if (is_array($value)) { + $kValues = Hash::flatten($value, '.'); + foreach ($kValues as $k2 => $v) { + $result[] = "$k2 = " . $this->_value($v); + } + } + if ($isSection) { + $result[] = ''; + } + } + $contents = trim(implode("\n", $result)); + + $filename = $this->_getFilePath($key); + return file_put_contents($filename, $contents); + } + + /** + * Converts a value into the ini equivalent + * + * @param mixed $val Value to export. + * @return string String value for ini file. + */ + protected function _value($val) + { + if ($val === null) { + return 'null'; + } + if ($val === true) { + return 'true'; + } + if ($val === false) { + return 'false'; + } + return (string)$val; + } } diff --git a/lib/Cake/Configure/PhpReader.php b/lib/Cake/Configure/PhpReader.php index 40176ce9..eaf101e5 100755 --- a/lib/Cake/Configure/PhpReader.php +++ b/lib/Cake/Configure/PhpReader.php @@ -26,93 +26,98 @@ * * @package Cake.Configure */ -class PhpReader implements ConfigReaderInterface { +class PhpReader implements ConfigReaderInterface +{ -/** - * The path this reader finds files on. - * - * @var string - */ - protected $_path = null; + /** + * The path this reader finds files on. + * + * @var string + */ + protected $_path = null; -/** - * Constructor for PHP Config file reading. - * - * @param string $path The path to read config files from. Defaults to CONFIG - */ - public function __construct($path = null) { - if (!$path) { - $path = CONFIG; - } - $this->_path = $path; - } + /** + * Constructor for PHP Config file reading. + * + * @param string $path The path to read config files from. Defaults to CONFIG + */ + public function __construct($path = null) + { + if (!$path) { + $path = CONFIG; + } + $this->_path = $path; + } -/** - * Read a config file and return its contents. - * - * Files with `.` in the name will be treated as values in plugins. Instead of reading from - * the initialized path, plugin keys will be located using CakePlugin::path(). - * - * @param string $key The identifier to read from. If the key has a . it will be treated - * as a plugin prefix. - * @return array Parsed configuration values. - * @throws ConfigureException when files don't exist or they don't contain `$config`. - * Or when files contain '..' as this could lead to abusive reads. - */ - public function read($key) { - if (strpos($key, '..') !== false) { - throw new ConfigureException(__d('cake_dev', 'Cannot load configuration files with ../ in them.')); - } + /** + * Read a config file and return its contents. + * + * Files with `.` in the name will be treated as values in plugins. Instead of reading from + * the initialized path, plugin keys will be located using CakePlugin::path(). + * + * @param string $key The identifier to read from. If the key has a . it will be treated + * as a plugin prefix. + * @return array Parsed configuration values. + * @throws ConfigureException when files don't exist or they don't contain `$config`. + * Or when files contain '..' as this could lead to abusive reads. + */ + public function read($key) + { + if (strpos($key, '..') !== false) { + throw new ConfigureException(__d('cake_dev', 'Cannot load configuration files with ../ in them.')); + } - $file = $this->_getFilePath($key); - if (!is_file(realpath($file))) { - throw new ConfigureException(__d('cake_dev', 'Could not load configuration file: %s', $file)); - } + $file = $this->_getFilePath($key); + if (!is_file(realpath($file))) { + throw new ConfigureException(__d('cake_dev', 'Could not load configuration file: %s', $file)); + } - include $file; - if (!isset($config)) { - throw new ConfigureException(__d('cake_dev', 'No variable %s found in %s', '$config', $file)); - } - return $config; - } + include $file; + if (!isset($config)) { + throw new ConfigureException(__d('cake_dev', 'No variable %s found in %s', '$config', $file)); + } + return $config; + } -/** - * Converts the provided $data into a string of PHP code that can - * be used saved into a file and loaded later. - * - * @param string $key The identifier to write to. If the key has a . it will be treated - * as a plugin prefix. - * @param array $data Data to dump. - * @return int Bytes saved. - */ - public function dump($key, $data) { - $contents = '_getFilePath($key); - return file_put_contents($filename, $contents); - } + if ($plugin) { + $file = CakePlugin::path($plugin) . 'Config' . DS . $key; + } else { + $file = $this->_path . $key; + } -/** - * Get file path - * - * @param string $key The identifier to write to. If the key has a . it will be treated - * as a plugin prefix. - * @return string Full file path - */ - protected function _getFilePath($key) { - if (substr($key, -4) === '.php') { - $key = substr($key, 0, -4); - } - list($plugin, $key) = pluginSplit($key); - $key .= '.php'; + return $file; + } - if ($plugin) { - $file = CakePlugin::path($plugin) . 'Config' . DS . $key; - } else { - $file = $this->_path . $key; - } + /** + * Converts the provided $data into a string of PHP code that can + * be used saved into a file and loaded later. + * + * @param string $key The identifier to write to. If the key has a . it will be treated + * as a plugin prefix. + * @param array $data Data to dump. + * @return int Bytes saved. + */ + public function dump($key, $data) + { + $contents = '_getFilePath($key); + return file_put_contents($filename, $contents); + } } diff --git a/lib/Cake/Console/Command/AclShell.php b/lib/Cake/Console/Command/AclShell.php index 10db318e..3d4c9b5b 100755 --- a/lib/Cake/Console/Command/AclShell.php +++ b/lib/Cake/Console/Command/AclShell.php @@ -28,592 +28,612 @@ * * @package Cake.Console.Command */ -class AclShell extends AppShell { - -/** - * Contains instance of AclComponent - * - * @var AclComponent - */ - public $Acl; - -/** - * Contains arguments parsed from the command line. - * - * @var array - */ - public $args; - -/** - * Contains database source to use - * - * @var string - */ - public $connection = 'default'; - -/** - * Contains tasks to load and instantiate - * - * @var array - */ - public $tasks = array('DbConfig'); - -/** - * Override startup of the Shell - * - * @return void - */ - public function startup() { - parent::startup(); - if (isset($this->params['connection'])) { - $this->connection = $this->params['connection']; - } - - $class = Configure::read('Acl.classname'); - list($plugin, $class) = pluginSplit($class, true); - App::uses($class, $plugin . 'Controller/Component/Acl'); - if (!in_array($class, array('DbAcl', 'DB_ACL')) && !is_subclass_of($class, 'DbAcl')) { - $out = "--------------------------------------------------\n"; - $out .= __d('cake_console', 'Error: Your current CakePHP configuration is set to an ACL implementation other than DB.') . "\n"; - $out .= __d('cake_console', 'Please change your core config to reflect your decision to use DbAcl before attempting to use this script') . "\n"; - $out .= "--------------------------------------------------\n"; - $out .= __d('cake_console', 'Current ACL Classname: %s', $class) . "\n"; - $out .= "--------------------------------------------------\n"; - $this->err($out); - return $this->_stop(); - } - - if ($this->command) { - if (!config('database')) { - $this->out(__d('cake_console', 'Your database configuration was not found. Take a moment to create one.')); - $this->args = null; - return $this->DbConfig->execute(); - } - require_once CONFIG . 'database.php'; - - if (!in_array($this->command, array('initdb'))) { - $collection = new ComponentCollection(); - $this->Acl = new AclComponent($collection); - $controller = new Controller(); - $this->Acl->startup($controller); - } - } - } - -/** - * Override main() for help message hook - * - * @return void - */ - public function main() { - $this->out($this->OptionParser->help()); - } - -/** - * Creates an ARO/ACO node - * - * @return void - */ - public function create() { - extract($this->_dataVars()); - - $class = ucfirst($this->args[0]); - $parent = $this->parseIdentifier($this->args[1]); - - if (!empty($parent) && $parent !== '/' && $parent !== 'root') { - $parent = $this->_getNodeId($class, $parent); - } else { - $parent = null; - } - - $data = $this->parseIdentifier($this->args[2]); - if (is_string($data) && $data !== '/') { - $data = array('alias' => $data); - } elseif (is_string($data)) { - $this->error(__d('cake_console', '/ can not be used as an alias!') . __d('cake_console', " / is the root, please supply a sub alias")); - } - - $data['parent_id'] = $parent; - $this->Acl->{$class}->create(); - if ($this->Acl->{$class}->save($data)) { - $this->out(__d('cake_console', "New %s '%s' created.", $class, $this->args[2]), 2); - } else { - $this->err(__d('cake_console', "There was a problem creating a new %s '%s'.", $class, $this->args[2])); - } - } - -/** - * Delete an ARO/ACO node. Note there may be (as a result of poor configuration) - * multiple records with the same logical identifier. All are deleted. - * - * @return void - */ - public function delete() { - extract($this->_dataVars()); - - $identifier = $this->parseIdentifier($this->args[1]); - if (is_string($identifier)) { - $identifier = array('alias' => $identifier); - } - - if ($this->Acl->{$class}->find('all', array('conditions' => $identifier))) { - if (!$this->Acl->{$class}->deleteAll($identifier)) { - $this->error(__d('cake_console', 'Node Not Deleted. ') . __d('cake_console', 'There was an error deleting the %s.', $class) . "\n"); - } - $this->out(__d('cake_console', '%s deleted.', $class), 2); - } else { - $this->error(__d('cake_console', 'Node Not Deleted. ') . __d('cake_console', 'There was an error deleting the %s. Node does not exist.', $class) . "\n"); - } - } - -/** - * Set parent for an ARO/ACO node. - * - * @return void - */ - public function setParent() { - extract($this->_dataVars()); - $target = $this->parseIdentifier($this->args[1]); - $parent = $this->parseIdentifier($this->args[2]); - - $data = array( - $class => array( - 'id' => $this->_getNodeId($class, $target), - 'parent_id' => $this->_getNodeId($class, $parent) - ) - ); - $this->Acl->{$class}->create(); - if (!$this->Acl->{$class}->save($data)) { - $this->out(__d('cake_console', 'Error in setting new parent. Please make sure the parent node exists, and is not a descendant of the node specified.')); - } else { - $this->out(__d('cake_console', 'Node parent set to %s', $this->args[2]) . "\n"); - } - } - -/** - * Get path to specified ARO/ACO node. - * - * @return void - */ - public function getPath() { - extract($this->_dataVars()); - $identifier = $this->parseIdentifier($this->args[1]); - - $id = $this->_getNodeId($class, $identifier); - $nodes = $this->Acl->{$class}->getPath($id); - - if (empty($nodes)) { - $this->error( - __d('cake_console', "Supplied Node '%s' not found", $this->args[1]), - __d('cake_console', 'No tree returned.') - ); - } - $this->out(__d('cake_console', 'Path:')); - $this->hr(); - for ($i = 0, $len = count($nodes); $i < $len; $i++) { - $this->_outputNode($class, $nodes[$i], $i); - } - } - -/** - * Outputs a single node, Either using the alias or Model.key - * - * @param string $class Class name that is being used. - * @param array $node Array of node information. - * @param int $indent indent level. - * @return void - */ - protected function _outputNode($class, $node, $indent) { - $indent = str_repeat(' ', $indent); - $data = $node[$class]; - if ($data['alias']) { - $this->out($indent . "[" . $data['id'] . "] " . $data['alias']); - } else { - $this->out($indent . "[" . $data['id'] . "] " . $data['model'] . '.' . $data['foreign_key']); - } - } - -/** - * Check permission for a given ARO to a given ACO. - * - * @return void - */ - public function check() { - extract($this->_getParams()); - - if ($this->Acl->check($aro, $aco, $action)) { - $this->out(__d('cake_console', '%s is allowed.', $aroName)); - } else { - $this->out(__d('cake_console', '%s is not allowed.', $aroName)); - } - } - -/** - * Grant permission for a given ARO to a given ACO. - * - * @return void - */ - public function grant() { - extract($this->_getParams()); - - if ($this->Acl->allow($aro, $aco, $action)) { - $this->out(__d('cake_console', 'Permission granted.')); - } else { - $this->out(__d('cake_console', 'Permission was not granted.')); - } - } - -/** - * Deny access for an ARO to an ACO. - * - * @return void - */ - public function deny() { - extract($this->_getParams()); - - if ($this->Acl->deny($aro, $aco, $action)) { - $this->out(__d('cake_console', 'Permission denied.')); - } else { - $this->out(__d('cake_console', 'Permission was not denied.')); - } - } - -/** - * Set an ARO to inherit permission to an ACO. - * - * @return void - */ - public function inherit() { - extract($this->_getParams()); - - if ($this->Acl->inherit($aro, $aco, $action)) { - $this->out(__d('cake_console', 'Permission inherited.')); - } else { - $this->out(__d('cake_console', 'Permission was not inherited.')); - } - } - -/** - * Show a specific ARO/ACO node. - * - * @return void - */ - public function view() { - extract($this->_dataVars()); - - if (isset($this->args[1])) { - $identity = $this->parseIdentifier($this->args[1]); - - $topNode = $this->Acl->{$class}->find('first', array( - 'conditions' => array($class . '.id' => $this->_getNodeId($class, $identity)) - )); - - $nodes = $this->Acl->{$class}->find('all', array( - 'conditions' => array( - $class . '.lft >=' => $topNode[$class]['lft'], - $class . '.lft <=' => $topNode[$class]['rght'] - ), - 'order' => $class . '.lft ASC' - )); - } else { - $nodes = $this->Acl->{$class}->find('all', array('order' => $class . '.lft ASC')); - } - - if (empty($nodes)) { - if (isset($this->args[1])) { - $this->error(__d('cake_console', '%s not found', $this->args[1]), __d('cake_console', 'No tree returned.')); - } elseif (isset($this->args[0])) { - $this->error(__d('cake_console', '%s not found', $this->args[0]), __d('cake_console', 'No tree returned.')); - } - } - $this->out($class . ' tree:'); - $this->hr(); - - $stack = array(); - $last = null; - - foreach ($nodes as $n) { - $stack[] = $n; - if (!empty($last)) { - $end = end($stack); - if ($end[$class]['rght'] > $last) { - foreach ($stack as $k => $v) { - $end = end($stack); - if ($v[$class]['rght'] < $end[$class]['rght']) { - unset($stack[$k]); - } - } - } - } - $last = $n[$class]['rght']; - $count = count($stack); - - $this->_outputNode($class, $n, $count); - } - $this->hr(); - } - -/** - * Initialize ACL database. - * - * @return mixed - */ - public function initdb() { - return $this->dispatchShell('schema create DbAcl'); - } - -/** - * Gets the option parser instance and configures it. - * - * @return ConsoleOptionParser - */ - public function getOptionParser() { - $parser = parent::getOptionParser(); - - $type = array( - 'choices' => array('aro', 'aco'), - 'required' => true, - 'help' => __d('cake_console', 'Type of node to create.') - ); - - $parser->description( - __d('cake_console', 'A console tool for managing the DbAcl') - )->addSubcommand('create', array( - 'help' => __d('cake_console', 'Create a new ACL node'), - 'parser' => array( - 'description' => __d('cake_console', 'Creates a new ACL object under the parent'), - 'epilog' => __d('cake_console', 'You can use `root` as the parent when creating nodes to create top level nodes.'), - 'arguments' => array( - 'type' => $type, - 'parent' => array( - 'help' => __d('cake_console', 'The node selector for the parent.'), - 'required' => true - ), - 'alias' => array( - 'help' => __d('cake_console', 'The alias to use for the newly created node.'), - 'required' => true - ) - ) - ) - ))->addSubcommand('delete', array( - 'help' => __d('cake_console', 'Deletes the ACL object with the given reference'), - 'parser' => array( - 'description' => __d('cake_console', 'Delete an ACL node.'), - 'arguments' => array( - 'type' => $type, - 'node' => array( - 'help' => __d('cake_console', 'The node identifier to delete.'), - 'required' => true, - ) - ) - ) - ))->addSubcommand('setparent', array( - 'help' => __d('cake_console', 'Moves the ACL node under a new parent.'), - 'parser' => array( - 'description' => __d('cake_console', 'Moves the ACL object specified by beneath '), - 'arguments' => array( - 'type' => $type, - 'node' => array( - 'help' => __d('cake_console', 'The node to move'), - 'required' => true, - ), - 'parent' => array( - 'help' => __d('cake_console', 'The new parent for .'), - 'required' => true - ) - ) - ) - ))->addSubcommand('getpath', array( - 'help' => __d('cake_console', 'Print out the path to an ACL node.'), - 'parser' => array( - 'description' => array( - __d('cake_console', "Returns the path to the ACL object specified by ."), - __d('cake_console', "This command is useful in determining the inheritance of permissions for a certain object in the tree.") - ), - 'arguments' => array( - 'type' => $type, - 'node' => array( - 'help' => __d('cake_console', 'The node to get the path of'), - 'required' => true, - ) - ) - ) - ))->addSubcommand('check', array( - 'help' => __d('cake_console', 'Check the permissions between an ACO and ARO.'), - 'parser' => array( - 'description' => array( - __d('cake_console', 'Use this command to check ACL permissions.') - ), - 'arguments' => array( - 'aro' => array('help' => __d('cake_console', 'ARO to check.'), 'required' => true), - 'aco' => array('help' => __d('cake_console', 'ACO to check.'), 'required' => true), - 'action' => array('help' => __d('cake_console', 'Action to check'), 'default' => 'all') - ) - ) - ))->addSubcommand('grant', array( - 'help' => __d('cake_console', 'Grant an ARO permissions to an ACO.'), - 'parser' => array( - 'description' => array( - __d('cake_console', 'Use this command to grant ACL permissions. Once executed, the ARO specified (and its children, if any) will have ALLOW access to the specified ACO action (and the ACO\'s children, if any).') - ), - 'arguments' => array( - 'aro' => array('help' => __d('cake_console', 'ARO to grant permission to.'), 'required' => true), - 'aco' => array('help' => __d('cake_console', 'ACO to grant access to.'), 'required' => true), - 'action' => array('help' => __d('cake_console', 'Action to grant'), 'default' => 'all') - ) - ) - ))->addSubcommand('deny', array( - 'help' => __d('cake_console', 'Deny an ARO permissions to an ACO.'), - 'parser' => array( - 'description' => array( - __d('cake_console', 'Use this command to deny ACL permissions. Once executed, the ARO specified (and its children, if any) will have DENY access to the specified ACO action (and the ACO\'s children, if any).') - ), - 'arguments' => array( - 'aro' => array('help' => __d('cake_console', 'ARO to deny.'), 'required' => true), - 'aco' => array('help' => __d('cake_console', 'ACO to deny.'), 'required' => true), - 'action' => array('help' => __d('cake_console', 'Action to deny'), 'default' => 'all') - ) - ) - ))->addSubcommand('inherit', array( - 'help' => __d('cake_console', 'Inherit an ARO\'s parent permissions.'), - 'parser' => array( - 'description' => array( - __d('cake_console', "Use this command to force a child ARO object to inherit its permissions settings from its parent.") - ), - 'arguments' => array( - 'aro' => array('help' => __d('cake_console', 'ARO to have permissions inherit.'), 'required' => true), - 'aco' => array('help' => __d('cake_console', 'ACO to inherit permissions on.'), 'required' => true), - 'action' => array('help' => __d('cake_console', 'Action to inherit'), 'default' => 'all') - ) - ) - ))->addSubcommand('view', array( - 'help' => __d('cake_console', 'View a tree or a single node\'s subtree.'), - 'parser' => array( - 'description' => array( - __d('cake_console', "The view command will return the ARO or ACO tree."), - __d('cake_console', "The optional node parameter allows you to return"), - __d('cake_console', "only a portion of the requested tree.") - ), - 'arguments' => array( - 'type' => $type, - 'node' => array('help' => __d('cake_console', 'The optional node to view the subtree of.')) - ) - ) - ))->addSubcommand('initdb', array( - 'help' => __d('cake_console', 'Initialize the DbAcl tables. Uses this command : cake schema create DbAcl') - ))->epilog(array( - 'Node and parent arguments can be in one of the following formats:', - '', - ' - . - The node will be bound to a specific record of the given model.', - '', - ' - - The node will be given a string alias (or path, in the case of )', - " i.e. 'John'. When used with , this takes the form of an alias path,", - " i.e. //.", - '', - "To add a node at the root level, enter 'root' or '/' as the parameter." - )); - - return $parser; - } - -/** - * Checks that given node exists - * - * @return bool Success - */ - public function nodeExists() { - if (!isset($this->args[0]) || !isset($this->args[1])) { - return false; - } - $dataVars = $this->_dataVars($this->args[0]); - extract($dataVars); - $key = is_numeric($this->args[1]) ? $dataVars['secondary_id'] : 'alias'; - $conditions = array($class . '.' . $key => $this->args[1]); - $possibility = $this->Acl->{$class}->find('all', compact('conditions')); - if (empty($possibility)) { - $this->error(__d('cake_console', '%s not found', $this->args[1]), __d('cake_console', 'No tree returned.')); - } - return $possibility; - } - -/** - * Parse an identifier into Model.foreignKey or an alias. - * Takes an identifier determines its type and returns the result as used by other methods. - * - * @param string $identifier Identifier to parse - * @return mixed a string for aliases, and an array for model.foreignKey - */ - public function parseIdentifier($identifier) { - if (preg_match('/^([\w]+)\.(.*)$/', $identifier, $matches)) { - return array( - 'model' => $matches[1], - 'foreign_key' => $matches[2], - ); - } - return $identifier; - } - -/** - * Get the node for a given identifier. $identifier can either be a string alias - * or an array of properties to use in AcoNode::node() - * - * @param string $class Class type you want (Aro/Aco) - * @param string|array|null $identifier A mixed identifier for finding the node, otherwise null. - * @return int Integer of NodeId. Will trigger an error if nothing is found. - */ - protected function _getNodeId($class, $identifier) { - $node = $this->Acl->{$class}->node($identifier); - if (empty($node)) { - if (is_array($identifier)) { - $identifier = var_export($identifier, true); - } - $this->error(__d('cake_console', 'Could not find node using reference "%s"', $identifier)); - return null; - } - return Hash::get($node, "0.{$class}.id"); - } - -/** - * get params for standard Acl methods - * - * @return array aro, aco, action - */ - protected function _getParams() { - $aro = is_numeric($this->args[0]) ? (int)$this->args[0] : $this->args[0]; - $aco = is_numeric($this->args[1]) ? (int)$this->args[1] : $this->args[1]; - $aroName = $aro; - $acoName = $aco; - - if (is_string($aro)) { - $aro = $this->parseIdentifier($aro); - } - if (is_string($aco)) { - $aco = $this->parseIdentifier($aco); - } - $action = '*'; - if (isset($this->args[2]) && !in_array($this->args[2], array('', 'all'))) { - $action = $this->args[2]; - } - return compact('aro', 'aco', 'action', 'aroName', 'acoName'); - } - -/** - * Build data parameters based on node type - * - * @param string $type Node type (ARO/ACO) - * @return array Variables - */ - protected function _dataVars($type = null) { - if (!$type) { - $type = $this->args[0]; - } - $vars = array(); - $class = ucwords($type); - $vars['secondary_id'] = (strtolower($class) === 'aro') ? 'foreign_key' : 'object_id'; - $vars['data_name'] = $type; - $vars['table_name'] = $type . 's'; - $vars['class'] = $class; - return $vars; - } +class AclShell extends AppShell +{ + + /** + * Contains instance of AclComponent + * + * @var AclComponent + */ + public $Acl; + + /** + * Contains arguments parsed from the command line. + * + * @var array + */ + public $args; + + /** + * Contains database source to use + * + * @var string + */ + public $connection = 'default'; + + /** + * Contains tasks to load and instantiate + * + * @var array + */ + public $tasks = ['DbConfig']; + + /** + * Override startup of the Shell + * + * @return void + */ + public function startup() + { + parent::startup(); + if (isset($this->params['connection'])) { + $this->connection = $this->params['connection']; + } + + $class = Configure::read('Acl.classname'); + list($plugin, $class) = pluginSplit($class, true); + App::uses($class, $plugin . 'Controller/Component/Acl'); + if (!in_array($class, ['DbAcl', 'DB_ACL']) && !is_subclass_of($class, 'DbAcl')) { + $out = "--------------------------------------------------\n"; + $out .= __d('cake_console', 'Error: Your current CakePHP configuration is set to an ACL implementation other than DB.') . "\n"; + $out .= __d('cake_console', 'Please change your core config to reflect your decision to use DbAcl before attempting to use this script') . "\n"; + $out .= "--------------------------------------------------\n"; + $out .= __d('cake_console', 'Current ACL Classname: %s', $class) . "\n"; + $out .= "--------------------------------------------------\n"; + $this->err($out); + return $this->_stop(); + } + + if ($this->command) { + if (!config('database')) { + $this->out(__d('cake_console', 'Your database configuration was not found. Take a moment to create one.')); + $this->args = null; + return $this->DbConfig->execute(); + } + require_once CONFIG . 'database.php'; + + if (!in_array($this->command, ['initdb'])) { + $collection = new ComponentCollection(); + $this->Acl = new AclComponent($collection); + $controller = new Controller(); + $this->Acl->startup($controller); + } + } + } + + /** + * Override main() for help message hook + * + * @return void + */ + public function main() + { + $this->out($this->OptionParser->help()); + } + + /** + * Creates an ARO/ACO node + * + * @return void + */ + public function create() + { + extract($this->_dataVars()); + + $class = ucfirst($this->args[0]); + $parent = $this->parseIdentifier($this->args[1]); + + if (!empty($parent) && $parent !== '/' && $parent !== 'root') { + $parent = $this->_getNodeId($class, $parent); + } else { + $parent = null; + } + + $data = $this->parseIdentifier($this->args[2]); + if (is_string($data) && $data !== '/') { + $data = ['alias' => $data]; + } else if (is_string($data)) { + $this->error(__d('cake_console', '/ can not be used as an alias!') . __d('cake_console', " / is the root, please supply a sub alias")); + } + + $data['parent_id'] = $parent; + $this->Acl->{$class}->create(); + if ($this->Acl->{$class}->save($data)) { + $this->out(__d('cake_console', "New %s '%s' created.", $class, $this->args[2]), 2); + } else { + $this->err(__d('cake_console', "There was a problem creating a new %s '%s'.", $class, $this->args[2])); + } + } + + /** + * Build data parameters based on node type + * + * @param string $type Node type (ARO/ACO) + * @return array Variables + */ + protected function _dataVars($type = null) + { + if (!$type) { + $type = $this->args[0]; + } + $vars = []; + $class = ucwords($type); + $vars['secondary_id'] = (strtolower($class) === 'aro') ? 'foreign_key' : 'object_id'; + $vars['data_name'] = $type; + $vars['table_name'] = $type . 's'; + $vars['class'] = $class; + return $vars; + } + + /** + * Parse an identifier into Model.foreignKey or an alias. + * Takes an identifier determines its type and returns the result as used by other methods. + * + * @param string $identifier Identifier to parse + * @return mixed a string for aliases, and an array for model.foreignKey + */ + public function parseIdentifier($identifier) + { + if (preg_match('/^([\w]+)\.(.*)$/', $identifier, $matches)) { + return [ + 'model' => $matches[1], + 'foreign_key' => $matches[2], + ]; + } + return $identifier; + } + + /** + * Get the node for a given identifier. $identifier can either be a string alias + * or an array of properties to use in AcoNode::node() + * + * @param string $class Class type you want (Aro/Aco) + * @param string|array|null $identifier A mixed identifier for finding the node, otherwise null. + * @return int Integer of NodeId. Will trigger an error if nothing is found. + */ + protected function _getNodeId($class, $identifier) + { + $node = $this->Acl->{$class}->node($identifier); + if (empty($node)) { + if (is_array($identifier)) { + $identifier = var_export($identifier, true); + } + $this->error(__d('cake_console', 'Could not find node using reference "%s"', $identifier)); + return null; + } + return Hash::get($node, "0.{$class}.id"); + } + + /** + * Delete an ARO/ACO node. Note there may be (as a result of poor configuration) + * multiple records with the same logical identifier. All are deleted. + * + * @return void + */ + public function delete() + { + extract($this->_dataVars()); + + $identifier = $this->parseIdentifier($this->args[1]); + if (is_string($identifier)) { + $identifier = ['alias' => $identifier]; + } + + if ($this->Acl->{$class}->find('all', ['conditions' => $identifier])) { + if (!$this->Acl->{$class}->deleteAll($identifier)) { + $this->error(__d('cake_console', 'Node Not Deleted. ') . __d('cake_console', 'There was an error deleting the %s.', $class) . "\n"); + } + $this->out(__d('cake_console', '%s deleted.', $class), 2); + } else { + $this->error(__d('cake_console', 'Node Not Deleted. ') . __d('cake_console', 'There was an error deleting the %s. Node does not exist.', $class) . "\n"); + } + } + + /** + * Set parent for an ARO/ACO node. + * + * @return void + */ + public function setParent() + { + extract($this->_dataVars()); + $target = $this->parseIdentifier($this->args[1]); + $parent = $this->parseIdentifier($this->args[2]); + + $data = [ + $class => [ + 'id' => $this->_getNodeId($class, $target), + 'parent_id' => $this->_getNodeId($class, $parent) + ] + ]; + $this->Acl->{$class}->create(); + if (!$this->Acl->{$class}->save($data)) { + $this->out(__d('cake_console', 'Error in setting new parent. Please make sure the parent node exists, and is not a descendant of the node specified.')); + } else { + $this->out(__d('cake_console', 'Node parent set to %s', $this->args[2]) . "\n"); + } + } + + /** + * Get path to specified ARO/ACO node. + * + * @return void + */ + public function getPath() + { + extract($this->_dataVars()); + $identifier = $this->parseIdentifier($this->args[1]); + + $id = $this->_getNodeId($class, $identifier); + $nodes = $this->Acl->{$class}->getPath($id); + + if (empty($nodes)) { + $this->error( + __d('cake_console', "Supplied Node '%s' not found", $this->args[1]), + __d('cake_console', 'No tree returned.') + ); + } + $this->out(__d('cake_console', 'Path:')); + $this->hr(); + for ($i = 0, $len = count($nodes); $i < $len; $i++) { + $this->_outputNode($class, $nodes[$i], $i); + } + } + + /** + * Outputs a single node, Either using the alias or Model.key + * + * @param string $class Class name that is being used. + * @param array $node Array of node information. + * @param int $indent indent level. + * @return void + */ + protected function _outputNode($class, $node, $indent) + { + $indent = str_repeat(' ', $indent); + $data = $node[$class]; + if ($data['alias']) { + $this->out($indent . "[" . $data['id'] . "] " . $data['alias']); + } else { + $this->out($indent . "[" . $data['id'] . "] " . $data['model'] . '.' . $data['foreign_key']); + } + } + + /** + * Check permission for a given ARO to a given ACO. + * + * @return void + */ + public function check() + { + extract($this->_getParams()); + + if ($this->Acl->check($aro, $aco, $action)) { + $this->out(__d('cake_console', '%s is allowed.', $aroName)); + } else { + $this->out(__d('cake_console', '%s is not allowed.', $aroName)); + } + } + + /** + * get params for standard Acl methods + * + * @return array aro, aco, action + */ + protected function _getParams() + { + $aro = is_numeric($this->args[0]) ? (int)$this->args[0] : $this->args[0]; + $aco = is_numeric($this->args[1]) ? (int)$this->args[1] : $this->args[1]; + $aroName = $aro; + $acoName = $aco; + + if (is_string($aro)) { + $aro = $this->parseIdentifier($aro); + } + if (is_string($aco)) { + $aco = $this->parseIdentifier($aco); + } + $action = '*'; + if (isset($this->args[2]) && !in_array($this->args[2], ['', 'all'])) { + $action = $this->args[2]; + } + return compact('aro', 'aco', 'action', 'aroName', 'acoName'); + } + + /** + * Grant permission for a given ARO to a given ACO. + * + * @return void + */ + public function grant() + { + extract($this->_getParams()); + + if ($this->Acl->allow($aro, $aco, $action)) { + $this->out(__d('cake_console', 'Permission granted.')); + } else { + $this->out(__d('cake_console', 'Permission was not granted.')); + } + } + + /** + * Deny access for an ARO to an ACO. + * + * @return void + */ + public function deny() + { + extract($this->_getParams()); + + if ($this->Acl->deny($aro, $aco, $action)) { + $this->out(__d('cake_console', 'Permission denied.')); + } else { + $this->out(__d('cake_console', 'Permission was not denied.')); + } + } + + /** + * Set an ARO to inherit permission to an ACO. + * + * @return void + */ + public function inherit() + { + extract($this->_getParams()); + + if ($this->Acl->inherit($aro, $aco, $action)) { + $this->out(__d('cake_console', 'Permission inherited.')); + } else { + $this->out(__d('cake_console', 'Permission was not inherited.')); + } + } + + /** + * Show a specific ARO/ACO node. + * + * @return void + */ + public function view() + { + extract($this->_dataVars()); + + if (isset($this->args[1])) { + $identity = $this->parseIdentifier($this->args[1]); + + $topNode = $this->Acl->{$class}->find('first', [ + 'conditions' => [$class . '.id' => $this->_getNodeId($class, $identity)] + ]); + + $nodes = $this->Acl->{$class}->find('all', [ + 'conditions' => [ + $class . '.lft >=' => $topNode[$class]['lft'], + $class . '.lft <=' => $topNode[$class]['rght'] + ], + 'order' => $class . '.lft ASC' + ]); + } else { + $nodes = $this->Acl->{$class}->find('all', ['order' => $class . '.lft ASC']); + } + + if (empty($nodes)) { + if (isset($this->args[1])) { + $this->error(__d('cake_console', '%s not found', $this->args[1]), __d('cake_console', 'No tree returned.')); + } else if (isset($this->args[0])) { + $this->error(__d('cake_console', '%s not found', $this->args[0]), __d('cake_console', 'No tree returned.')); + } + } + $this->out($class . ' tree:'); + $this->hr(); + + $stack = []; + $last = null; + + foreach ($nodes as $n) { + $stack[] = $n; + if (!empty($last)) { + $end = end($stack); + if ($end[$class]['rght'] > $last) { + foreach ($stack as $k => $v) { + $end = end($stack); + if ($v[$class]['rght'] < $end[$class]['rght']) { + unset($stack[$k]); + } + } + } + } + $last = $n[$class]['rght']; + $count = count($stack); + + $this->_outputNode($class, $n, $count); + } + $this->hr(); + } + + /** + * Initialize ACL database. + * + * @return mixed + */ + public function initdb() + { + return $this->dispatchShell('schema create DbAcl'); + } + + /** + * Gets the option parser instance and configures it. + * + * @return ConsoleOptionParser + */ + public function getOptionParser() + { + $parser = parent::getOptionParser(); + + $type = [ + 'choices' => ['aro', 'aco'], + 'required' => true, + 'help' => __d('cake_console', 'Type of node to create.') + ]; + + $parser->description( + __d('cake_console', 'A console tool for managing the DbAcl') + )->addSubcommand('create', [ + 'help' => __d('cake_console', 'Create a new ACL node'), + 'parser' => [ + 'description' => __d('cake_console', 'Creates a new ACL object under the parent'), + 'epilog' => __d('cake_console', 'You can use `root` as the parent when creating nodes to create top level nodes.'), + 'arguments' => [ + 'type' => $type, + 'parent' => [ + 'help' => __d('cake_console', 'The node selector for the parent.'), + 'required' => true + ], + 'alias' => [ + 'help' => __d('cake_console', 'The alias to use for the newly created node.'), + 'required' => true + ] + ] + ] + ])->addSubcommand('delete', [ + 'help' => __d('cake_console', 'Deletes the ACL object with the given reference'), + 'parser' => [ + 'description' => __d('cake_console', 'Delete an ACL node.'), + 'arguments' => [ + 'type' => $type, + 'node' => [ + 'help' => __d('cake_console', 'The node identifier to delete.'), + 'required' => true, + ] + ] + ] + ])->addSubcommand('setparent', [ + 'help' => __d('cake_console', 'Moves the ACL node under a new parent.'), + 'parser' => [ + 'description' => __d('cake_console', 'Moves the ACL object specified by beneath '), + 'arguments' => [ + 'type' => $type, + 'node' => [ + 'help' => __d('cake_console', 'The node to move'), + 'required' => true, + ], + 'parent' => [ + 'help' => __d('cake_console', 'The new parent for .'), + 'required' => true + ] + ] + ] + ])->addSubcommand('getpath', [ + 'help' => __d('cake_console', 'Print out the path to an ACL node.'), + 'parser' => [ + 'description' => [ + __d('cake_console', "Returns the path to the ACL object specified by ."), + __d('cake_console', "This command is useful in determining the inheritance of permissions for a certain object in the tree.") + ], + 'arguments' => [ + 'type' => $type, + 'node' => [ + 'help' => __d('cake_console', 'The node to get the path of'), + 'required' => true, + ] + ] + ] + ])->addSubcommand('check', [ + 'help' => __d('cake_console', 'Check the permissions between an ACO and ARO.'), + 'parser' => [ + 'description' => [ + __d('cake_console', 'Use this command to check ACL permissions.') + ], + 'arguments' => [ + 'aro' => ['help' => __d('cake_console', 'ARO to check.'), 'required' => true], + 'aco' => ['help' => __d('cake_console', 'ACO to check.'), 'required' => true], + 'action' => ['help' => __d('cake_console', 'Action to check'), 'default' => 'all'] + ] + ] + ])->addSubcommand('grant', [ + 'help' => __d('cake_console', 'Grant an ARO permissions to an ACO.'), + 'parser' => [ + 'description' => [ + __d('cake_console', 'Use this command to grant ACL permissions. Once executed, the ARO specified (and its children, if any) will have ALLOW access to the specified ACO action (and the ACO\'s children, if any).') + ], + 'arguments' => [ + 'aro' => ['help' => __d('cake_console', 'ARO to grant permission to.'), 'required' => true], + 'aco' => ['help' => __d('cake_console', 'ACO to grant access to.'), 'required' => true], + 'action' => ['help' => __d('cake_console', 'Action to grant'), 'default' => 'all'] + ] + ] + ])->addSubcommand('deny', [ + 'help' => __d('cake_console', 'Deny an ARO permissions to an ACO.'), + 'parser' => [ + 'description' => [ + __d('cake_console', 'Use this command to deny ACL permissions. Once executed, the ARO specified (and its children, if any) will have DENY access to the specified ACO action (and the ACO\'s children, if any).') + ], + 'arguments' => [ + 'aro' => ['help' => __d('cake_console', 'ARO to deny.'), 'required' => true], + 'aco' => ['help' => __d('cake_console', 'ACO to deny.'), 'required' => true], + 'action' => ['help' => __d('cake_console', 'Action to deny'), 'default' => 'all'] + ] + ] + ])->addSubcommand('inherit', [ + 'help' => __d('cake_console', 'Inherit an ARO\'s parent permissions.'), + 'parser' => [ + 'description' => [ + __d('cake_console', "Use this command to force a child ARO object to inherit its permissions settings from its parent.") + ], + 'arguments' => [ + 'aro' => ['help' => __d('cake_console', 'ARO to have permissions inherit.'), 'required' => true], + 'aco' => ['help' => __d('cake_console', 'ACO to inherit permissions on.'), 'required' => true], + 'action' => ['help' => __d('cake_console', 'Action to inherit'), 'default' => 'all'] + ] + ] + ])->addSubcommand('view', [ + 'help' => __d('cake_console', 'View a tree or a single node\'s subtree.'), + 'parser' => [ + 'description' => [ + __d('cake_console', "The view command will return the ARO or ACO tree."), + __d('cake_console', "The optional node parameter allows you to return"), + __d('cake_console', "only a portion of the requested tree.") + ], + 'arguments' => [ + 'type' => $type, + 'node' => ['help' => __d('cake_console', 'The optional node to view the subtree of.')] + ] + ] + ])->addSubcommand('initdb', [ + 'help' => __d('cake_console', 'Initialize the DbAcl tables. Uses this command : cake schema create DbAcl') + ])->epilog([ + 'Node and parent arguments can be in one of the following formats:', + '', + ' - . - The node will be bound to a specific record of the given model.', + '', + ' - - The node will be given a string alias (or path, in the case of )', + " i.e. 'John'. When used with , this takes the form of an alias path,", + " i.e. //.", + '', + "To add a node at the root level, enter 'root' or '/' as the parameter." + ]); + + return $parser; + } + + /** + * Checks that given node exists + * + * @return bool Success + */ + public function nodeExists() + { + if (!isset($this->args[0]) || !isset($this->args[1])) { + return false; + } + $dataVars = $this->_dataVars($this->args[0]); + extract($dataVars); + $key = is_numeric($this->args[1]) ? $dataVars['secondary_id'] : 'alias'; + $conditions = [$class . '.' . $key => $this->args[1]]; + $possibility = $this->Acl->{$class}->find('all', compact('conditions')); + if (empty($possibility)) { + $this->error(__d('cake_console', '%s not found', $this->args[1]), __d('cake_console', 'No tree returned.')); + } + return $possibility; + } } diff --git a/lib/Cake/Console/Command/ApiShell.php b/lib/Cake/Console/Command/ApiShell.php index 3c3dd1ce..fc6c45ae 100755 --- a/lib/Cake/Console/Command/ApiShell.php +++ b/lib/Cake/Console/Command/ApiShell.php @@ -27,216 +27,222 @@ * * @package Cake.Console.Command */ -class ApiShell extends AppShell { - -/** - * Map between short name for paths and real paths. - * - * @var array - */ - public $paths = array(); - -/** - * Override initialize of the Shell - * - * @return void - */ - public function initialize() { - $this->paths = array_merge($this->paths, array( - 'behavior' => CAKE . 'Model' . DS . 'Behavior' . DS, - 'cache' => CAKE . 'Cache' . DS, - 'controller' => CAKE . 'Controller' . DS, - 'component' => CAKE . 'Controller' . DS . 'Component' . DS, - 'helper' => CAKE . 'View' . DS . 'Helper' . DS, - 'model' => CAKE . 'Model' . DS, - 'view' => CAKE . 'View' . DS, - 'core' => CAKE - )); - } - -/** - * Override main() to handle action - * - * @return void - */ - public function main() { - if (empty($this->args)) { - return $this->out($this->OptionParser->help()); - } - - $type = strtolower($this->args[0]); - - if (isset($this->paths[$type])) { - $path = $this->paths[$type]; - } else { - $path = $this->paths['core']; - } - - $count = count($this->args); - if ($count > 1) { - $file = Inflector::underscore($this->args[1]); - $class = Inflector::camelize($this->args[1]); - } elseif ($count) { - $file = $type; - $class = Inflector::camelize($type); - } - $objects = App::objects('class', $path); - if (in_array($class, $objects)) { - if (in_array($type, array('behavior', 'component', 'helper')) && $type !== $file) { - if (!preg_match('/' . Inflector::camelize($type) . '$/', $class)) { - $class .= Inflector::camelize($type); - } - } - - } else { - $this->error(__d('cake_console', '%s not found', $class)); - } - - $parsed = $this->_parseClass($path . $class . '.php', $class); - - if (!empty($parsed)) { - if (isset($this->params['method'])) { - if (!isset($parsed[$this->params['method']])) { - $this->err(__d('cake_console', '%s::%s() could not be found', $class, $this->params['method'])); - return $this->_stop(); - } - $method = $parsed[$this->params['method']]; - $this->out($class . '::' . $method['method'] . $method['parameters']); - $this->hr(); - $this->out($method['comment'], true); - } else { - $this->out(ucwords($class)); - $this->hr(); - $i = 0; - foreach ($parsed as $method) { - $list[] = ++$i . ". " . $method['method'] . $method['parameters']; - } - $this->out($list); - - $methods = array_keys($parsed); - while ($number = strtolower($this->in(__d('cake_console', 'Select a number to see the more information about a specific method. q to quit. l to list.'), null, 'q'))) { - if ($number === 'q') { - $this->out(__d('cake_console', 'Done')); - return $this->_stop(); - } - - if ($number === 'l') { - $this->out($list); - } - - if (isset($methods[--$number])) { - $method = $parsed[$methods[$number]]; - $this->hr(); - $this->out($class . '::' . $method['method'] . $method['parameters']); - $this->hr(); - $this->out($method['comment'], true); - } - } - } - } - } - -/** - * Gets the option parser instance and configures it. - * - * @return ConsoleOptionParser - */ - public function getOptionParser() { - $parser = parent::getOptionParser(); - - $parser->description( - __d('cake_console', 'Lookup doc block comments for classes in CakePHP.') - )->addArgument('type', array( - 'help' => __d('cake_console', 'Either a full path or type of class (model, behavior, controller, component, view, helper)') - ))->addArgument('className', array( - 'help' => __d('cake_console', 'A CakePHP core class name (e.g: Component, HtmlHelper).') - ))->addOption('method', array( - 'short' => 'm', - 'help' => __d('cake_console', 'The specific method you want help on.') - )); - - return $parser; - } - -/** - * Show help for this shell. - * - * @return void - */ - public function help() { - $head = "Usage: cake api [] [-m ]\n"; - $head .= "-----------------------------------------------\n"; - $head .= "Parameters:\n\n"; - - $commands = array( - 'path' => "\t\n" . - "\t\tEither a full path or type of class (model, behavior, controller, component, view, helper).\n" . - "\t\tAvailable values:\n\n" . - "\t\tbehavior\tLook for class in CakePHP behavior path\n" . - "\t\tcache\tLook for class in CakePHP cache path\n" . - "\t\tcontroller\tLook for class in CakePHP controller path\n" . - "\t\tcomponent\tLook for class in CakePHP component path\n" . - "\t\thelper\tLook for class in CakePHP helper path\n" . - "\t\tmodel\tLook for class in CakePHP model path\n" . - "\t\tview\tLook for class in CakePHP view path\n", - 'className' => "\t\n" . - "\t\tA CakePHP core class name (e.g: Component, HtmlHelper).\n" - ); - - $this->out($head); - if (!isset($this->args[1])) { - foreach ($commands as $cmd) { - $this->out("{$cmd}\n\n"); - } - } elseif (isset($commands[strtolower($this->args[1])])) { - $this->out($commands[strtolower($this->args[1])] . "\n\n"); - } else { - $this->out(__d('cake_console', 'Command %s not found', $this->args[1])); - } - } - -/** - * Parse a given class (located on given file) and get public methods and their - * signatures. - * - * @param string $path File path - * @param string $class Class name - * @return array Methods and signatures indexed by method name - */ - protected function _parseClass($path, $class) { - $parsed = array(); - - if (!class_exists($class)) { - if (!include_once $path) { - $this->err(__d('cake_console', '%s could not be found', $path)); - } - } - - $reflection = new ReflectionClass($class); - - foreach ($reflection->getMethods() as $method) { - if (!$method->isPublic() || strpos($method->getName(), '_') === 0) { - continue; - } - if ($method->getDeclaringClass()->getName() != $class) { - continue; - } - $args = array(); - foreach ($method->getParameters() as $param) { - $paramString = '$' . $param->getName(); - if ($param->isDefaultValueAvailable()) { - $paramString .= ' = ' . str_replace("\n", '', var_export($param->getDefaultValue(), true)); - } - $args[] = $paramString; - } - $parsed[$method->getName()] = array( - 'comment' => str_replace(array('/*', '*/', '*'), '', $method->getDocComment()), - 'method' => $method->getName(), - 'parameters' => '(' . implode(', ', $args) . ')' - ); - } - ksort($parsed); - return $parsed; - } +class ApiShell extends AppShell +{ + + /** + * Map between short name for paths and real paths. + * + * @var array + */ + public $paths = []; + + /** + * Override initialize of the Shell + * + * @return void + */ + public function initialize() + { + $this->paths = array_merge($this->paths, [ + 'behavior' => CAKE . 'Model' . DS . 'Behavior' . DS, + 'cache' => CAKE . 'Cache' . DS, + 'controller' => CAKE . 'Controller' . DS, + 'component' => CAKE . 'Controller' . DS . 'Component' . DS, + 'helper' => CAKE . 'View' . DS . 'Helper' . DS, + 'model' => CAKE . 'Model' . DS, + 'view' => CAKE . 'View' . DS, + 'core' => CAKE + ]); + } + + /** + * Override main() to handle action + * + * @return void + */ + public function main() + { + if (empty($this->args)) { + return $this->out($this->OptionParser->help()); + } + + $type = strtolower($this->args[0]); + + if (isset($this->paths[$type])) { + $path = $this->paths[$type]; + } else { + $path = $this->paths['core']; + } + + $count = count($this->args); + if ($count > 1) { + $file = Inflector::underscore($this->args[1]); + $class = Inflector::camelize($this->args[1]); + } else if ($count) { + $file = $type; + $class = Inflector::camelize($type); + } + $objects = App::objects('class', $path); + if (in_array($class, $objects)) { + if (in_array($type, ['behavior', 'component', 'helper']) && $type !== $file) { + if (!preg_match('/' . Inflector::camelize($type) . '$/', $class)) { + $class .= Inflector::camelize($type); + } + } + + } else { + $this->error(__d('cake_console', '%s not found', $class)); + } + + $parsed = $this->_parseClass($path . $class . '.php', $class); + + if (!empty($parsed)) { + if (isset($this->params['method'])) { + if (!isset($parsed[$this->params['method']])) { + $this->err(__d('cake_console', '%s::%s() could not be found', $class, $this->params['method'])); + return $this->_stop(); + } + $method = $parsed[$this->params['method']]; + $this->out($class . '::' . $method['method'] . $method['parameters']); + $this->hr(); + $this->out($method['comment'], true); + } else { + $this->out(ucwords($class)); + $this->hr(); + $i = 0; + foreach ($parsed as $method) { + $list[] = ++$i . ". " . $method['method'] . $method['parameters']; + } + $this->out($list); + + $methods = array_keys($parsed); + while ($number = strtolower($this->in(__d('cake_console', 'Select a number to see the more information about a specific method. q to quit. l to list.'), null, 'q'))) { + if ($number === 'q') { + $this->out(__d('cake_console', 'Done')); + return $this->_stop(); + } + + if ($number === 'l') { + $this->out($list); + } + + if (isset($methods[--$number])) { + $method = $parsed[$methods[$number]]; + $this->hr(); + $this->out($class . '::' . $method['method'] . $method['parameters']); + $this->hr(); + $this->out($method['comment'], true); + } + } + } + } + } + + /** + * Parse a given class (located on given file) and get public methods and their + * signatures. + * + * @param string $path File path + * @param string $class Class name + * @return array Methods and signatures indexed by method name + */ + protected function _parseClass($path, $class) + { + $parsed = []; + + if (!class_exists($class)) { + if (!include_once $path) { + $this->err(__d('cake_console', '%s could not be found', $path)); + } + } + + $reflection = new ReflectionClass($class); + + foreach ($reflection->getMethods() as $method) { + if (!$method->isPublic() || strpos($method->getName(), '_') === 0) { + continue; + } + if ($method->getDeclaringClass()->getName() != $class) { + continue; + } + $args = []; + foreach ($method->getParameters() as $param) { + $paramString = '$' . $param->getName(); + if ($param->isDefaultValueAvailable()) { + $paramString .= ' = ' . str_replace("\n", '', var_export($param->getDefaultValue(), true)); + } + $args[] = $paramString; + } + $parsed[$method->getName()] = [ + 'comment' => str_replace(['/*', '*/', '*'], '', $method->getDocComment()), + 'method' => $method->getName(), + 'parameters' => '(' . implode(', ', $args) . ')' + ]; + } + ksort($parsed); + return $parsed; + } + + /** + * Gets the option parser instance and configures it. + * + * @return ConsoleOptionParser + */ + public function getOptionParser() + { + $parser = parent::getOptionParser(); + + $parser->description( + __d('cake_console', 'Lookup doc block comments for classes in CakePHP.') + )->addArgument('type', [ + 'help' => __d('cake_console', 'Either a full path or type of class (model, behavior, controller, component, view, helper)') + ])->addArgument('className', [ + 'help' => __d('cake_console', 'A CakePHP core class name (e.g: Component, HtmlHelper).') + ])->addOption('method', [ + 'short' => 'm', + 'help' => __d('cake_console', 'The specific method you want help on.') + ]); + + return $parser; + } + + /** + * Show help for this shell. + * + * @return void + */ + public function help() + { + $head = "Usage: cake api [] [-m ]\n"; + $head .= "-----------------------------------------------\n"; + $head .= "Parameters:\n\n"; + + $commands = [ + 'path' => "\t\n" . + "\t\tEither a full path or type of class (model, behavior, controller, component, view, helper).\n" . + "\t\tAvailable values:\n\n" . + "\t\tbehavior\tLook for class in CakePHP behavior path\n" . + "\t\tcache\tLook for class in CakePHP cache path\n" . + "\t\tcontroller\tLook for class in CakePHP controller path\n" . + "\t\tcomponent\tLook for class in CakePHP component path\n" . + "\t\thelper\tLook for class in CakePHP helper path\n" . + "\t\tmodel\tLook for class in CakePHP model path\n" . + "\t\tview\tLook for class in CakePHP view path\n", + 'className' => "\t\n" . + "\t\tA CakePHP core class name (e.g: Component, HtmlHelper).\n" + ]; + + $this->out($head); + if (!isset($this->args[1])) { + foreach ($commands as $cmd) { + $this->out("{$cmd}\n\n"); + } + } else if (isset($commands[strtolower($this->args[1])])) { + $this->out($commands[strtolower($this->args[1])] . "\n\n"); + } else { + $this->out(__d('cake_console', 'Command %s not found', $this->args[1])); + } + } } diff --git a/lib/Cake/Console/Command/AppShell.php b/lib/Cake/Console/Command/AppShell.php index 030ae49f..ef140093 100755 --- a/lib/Cake/Console/Command/AppShell.php +++ b/lib/Cake/Console/Command/AppShell.php @@ -25,6 +25,7 @@ * * @package app.Console.Command */ -class AppShell extends Shell { +class AppShell extends Shell +{ } diff --git a/lib/Cake/Console/Command/BakeShell.php b/lib/Cake/Console/Command/BakeShell.php index e0db7bc0..e1db5306 100755 --- a/lib/Cake/Console/Command/BakeShell.php +++ b/lib/Cake/Console/Command/BakeShell.php @@ -32,224 +32,229 @@ * @package Cake.Console.Command * @link https://book.cakephp.org/2.0/en/console-and-shells/code-generation-with-bake.html */ -class BakeShell extends AppShell { +class BakeShell extends AppShell +{ -/** - * Contains tasks to load and instantiate - * - * @var array - */ - public $tasks = array('Project', 'DbConfig', 'Model', 'Controller', 'View', 'Plugin', 'Fixture', 'Test'); + /** + * Contains tasks to load and instantiate + * + * @var array + */ + public $tasks = ['Project', 'DbConfig', 'Model', 'Controller', 'View', 'Plugin', 'Fixture', 'Test']; -/** - * The connection being used. - * - * @var string - */ - public $connection = 'default'; + /** + * The connection being used. + * + * @var string + */ + public $connection = 'default'; -/** - * Assign $this->connection to the active task if a connection param is set. - * - * @return void - */ - public function startup() { - parent::startup(); - Configure::write('debug', 2); - Configure::write('Cache.disable', 1); - - $task = Inflector::classify($this->command); - if (isset($this->{$task}) && !in_array($task, array('Project', 'DbConfig'))) { - if (isset($this->params['connection'])) { - $this->{$task}->connection = $this->params['connection']; - } - } - if (isset($this->params['connection'])) { - $this->connection = $this->params['connection']; - } - } + /** + * Assign $this->connection to the active task if a connection param is set. + * + * @return void + */ + public function startup() + { + parent::startup(); + Configure::write('debug', 2); + Configure::write('Cache.disable', 1); -/** - * Override main() to handle action - * - * @return mixed - */ - public function main() { - if (!is_dir($this->DbConfig->path)) { - $path = $this->Project->execute(); - if (!empty($path)) { - $this->DbConfig->path = $path . 'Config' . DS; - } else { - return false; - } - } - - if (!config('database')) { - $this->out(__d('cake_console', 'Your database configuration was not found. Take a moment to create one.')); - $this->args = null; - return $this->DbConfig->execute(); - } - $this->out(__d('cake_console', 'Interactive Bake Shell')); - $this->hr(); - $this->out(__d('cake_console', '[D]atabase Configuration')); - $this->out(__d('cake_console', '[M]odel')); - $this->out(__d('cake_console', '[V]iew')); - $this->out(__d('cake_console', '[C]ontroller')); - $this->out(__d('cake_console', '[P]roject')); - $this->out(__d('cake_console', '[F]ixture')); - $this->out(__d('cake_console', '[T]est case')); - $this->out(__d('cake_console', '[Q]uit')); - - $classToBake = strtoupper($this->in(__d('cake_console', 'What would you like to Bake?'), array('D', 'M', 'V', 'C', 'P', 'F', 'T', 'Q'))); - switch ($classToBake) { - case 'D': - $this->DbConfig->execute(); - break; - case 'M': - $this->Model->execute(); - break; - case 'V': - $this->View->execute(); - break; - case 'C': - $this->Controller->execute(); - break; - case 'P': - $this->Project->execute(); - break; - case 'F': - $this->Fixture->execute(); - break; - case 'T': - $this->Test->execute(); - break; - case 'Q': - return $this->_stop(); - default: - $this->out(__d('cake_console', 'You have made an invalid selection. Please choose a type of class to Bake by entering D, M, V, F, T, or C.')); - } - $this->hr(); - $this->main(); - } + $task = Inflector::classify($this->command); + if (isset($this->{$task}) && !in_array($task, ['Project', 'DbConfig'])) { + if (isset($this->params['connection'])) { + $this->{$task}->connection = $this->params['connection']; + } + } + if (isset($this->params['connection'])) { + $this->connection = $this->params['connection']; + } + } -/** - * Quickly bake the MVC - * - * @return void - */ - public function all() { - $this->out('Bake All'); - $this->hr(); - - if (!isset($this->params['connection']) && empty($this->connection)) { - $this->connection = $this->DbConfig->getConfig(); - } - - if (empty($this->args)) { - $this->Model->interactive = true; - $name = $this->Model->getName($this->connection); - } - - foreach (array('Model', 'Controller', 'View') as $task) { - $this->{$task}->connection = $this->connection; - $this->{$task}->interactive = false; - } - - if (!empty($this->args[0])) { - $name = $this->args[0]; - } - - $modelExists = false; - $model = $this->_modelName($name); - - App::uses('AppModel', 'Model'); - App::uses($model, 'Model'); - if (class_exists($model)) { - $object = new $model(); - $modelExists = true; - } else { - $object = new Model(array('name' => $name, 'ds' => $this->connection)); - } - - $modelBaked = $this->Model->bake($object, false); - - if ($modelBaked && $modelExists === false) { - if ($this->_checkUnitTest()) { - $this->Model->bakeFixture($model); - $this->Model->bakeTest($model); - } - $modelExists = true; - } - - if ($modelExists === true) { - $controller = $this->_controllerName($name); - if ($this->Controller->bake($controller, $this->Controller->bakeActions($controller))) { - if ($this->_checkUnitTest()) { - $this->Controller->bakeTest($controller); - } - } - App::uses($controller . 'Controller', 'Controller'); - if (class_exists($controller . 'Controller')) { - $this->View->args = array($name); - $this->View->execute(); - } - $this->out('', 1, Shell::QUIET); - $this->out(__d('cake_console', 'Bake All complete'), 1, Shell::QUIET); - array_shift($this->args); - } else { - $this->error(__d('cake_console', 'Bake All could not continue without a valid model')); - } - return $this->_stop(); - } + /** + * Override main() to handle action + * + * @return mixed + */ + public function main() + { + if (!is_dir($this->DbConfig->path)) { + $path = $this->Project->execute(); + if (!empty($path)) { + $this->DbConfig->path = $path . 'Config' . DS; + } else { + return false; + } + } -/** - * Gets the option parser instance and configures it. - * - * @return ConsoleOptionParser - */ - public function getOptionParser() { - $parser = parent::getOptionParser(); - - $parser->description( - __d('cake_console', 'The Bake script generates controllers, views and models for your application.' . - ' If run with no command line arguments, Bake guides the user through the class creation process.' . - ' You can customize the generation process by telling Bake where different parts of your application are using command line arguments.') - )->addSubcommand('all', array( - 'help' => __d('cake_console', 'Bake a complete MVC. optional of a Model') - ))->addSubcommand('project', array( - 'help' => __d('cake_console', 'Bake a new app folder in the path supplied or in current directory if no path is specified'), - 'parser' => $this->Project->getOptionParser() - ))->addSubcommand('plugin', array( - 'help' => __d('cake_console', 'Bake a new plugin folder in the path supplied or in current directory if no path is specified.'), - 'parser' => $this->Plugin->getOptionParser() - ))->addSubcommand('db_config', array( - 'help' => __d('cake_console', 'Bake a database.php file in config directory.'), - 'parser' => $this->DbConfig->getOptionParser() - ))->addSubcommand('model', array( - 'help' => __d('cake_console', 'Bake a model.'), - 'parser' => $this->Model->getOptionParser() - ))->addSubcommand('view', array( - 'help' => __d('cake_console', 'Bake views for controllers.'), - 'parser' => $this->View->getOptionParser() - ))->addSubcommand('controller', array( - 'help' => __d('cake_console', 'Bake a controller.'), - 'parser' => $this->Controller->getOptionParser() - ))->addSubcommand('fixture', array( - 'help' => __d('cake_console', 'Bake a fixture.'), - 'parser' => $this->Fixture->getOptionParser() - ))->addSubcommand('test', array( - 'help' => __d('cake_console', 'Bake a unit test.'), - 'parser' => $this->Test->getOptionParser() - ))->addOption('connection', array( - 'help' => __d('cake_console', 'Database connection to use in conjunction with `bake all`.'), - 'short' => 'c', - 'default' => 'default' - ))->addOption('theme', array( - 'short' => 't', - 'help' => __d('cake_console', 'Theme to use when baking code.') - )); - - return $parser; - } + if (!config('database')) { + $this->out(__d('cake_console', 'Your database configuration was not found. Take a moment to create one.')); + $this->args = null; + return $this->DbConfig->execute(); + } + $this->out(__d('cake_console', 'Interactive Bake Shell')); + $this->hr(); + $this->out(__d('cake_console', '[D]atabase Configuration')); + $this->out(__d('cake_console', '[M]odel')); + $this->out(__d('cake_console', '[V]iew')); + $this->out(__d('cake_console', '[C]ontroller')); + $this->out(__d('cake_console', '[P]roject')); + $this->out(__d('cake_console', '[F]ixture')); + $this->out(__d('cake_console', '[T]est case')); + $this->out(__d('cake_console', '[Q]uit')); + + $classToBake = strtoupper($this->in(__d('cake_console', 'What would you like to Bake?'), ['D', 'M', 'V', 'C', 'P', 'F', 'T', 'Q'])); + switch ($classToBake) { + case 'D': + $this->DbConfig->execute(); + break; + case 'M': + $this->Model->execute(); + break; + case 'V': + $this->View->execute(); + break; + case 'C': + $this->Controller->execute(); + break; + case 'P': + $this->Project->execute(); + break; + case 'F': + $this->Fixture->execute(); + break; + case 'T': + $this->Test->execute(); + break; + case 'Q': + return $this->_stop(); + default: + $this->out(__d('cake_console', 'You have made an invalid selection. Please choose a type of class to Bake by entering D, M, V, F, T, or C.')); + } + $this->hr(); + $this->main(); + } + + /** + * Quickly bake the MVC + * + * @return void + */ + public function all() + { + $this->out('Bake All'); + $this->hr(); + + if (!isset($this->params['connection']) && empty($this->connection)) { + $this->connection = $this->DbConfig->getConfig(); + } + + if (empty($this->args)) { + $this->Model->interactive = true; + $name = $this->Model->getName($this->connection); + } + + foreach (['Model', 'Controller', 'View'] as $task) { + $this->{$task}->connection = $this->connection; + $this->{$task}->interactive = false; + } + + if (!empty($this->args[0])) { + $name = $this->args[0]; + } + + $modelExists = false; + $model = $this->_modelName($name); + + App::uses('AppModel', 'Model'); + App::uses($model, 'Model'); + if (class_exists($model)) { + $object = new $model(); + $modelExists = true; + } else { + $object = new Model(['name' => $name, 'ds' => $this->connection]); + } + + $modelBaked = $this->Model->bake($object, false); + + if ($modelBaked && $modelExists === false) { + if ($this->_checkUnitTest()) { + $this->Model->bakeFixture($model); + $this->Model->bakeTest($model); + } + $modelExists = true; + } + + if ($modelExists === true) { + $controller = $this->_controllerName($name); + if ($this->Controller->bake($controller, $this->Controller->bakeActions($controller))) { + if ($this->_checkUnitTest()) { + $this->Controller->bakeTest($controller); + } + } + App::uses($controller . 'Controller', 'Controller'); + if (class_exists($controller . 'Controller')) { + $this->View->args = [$name]; + $this->View->execute(); + } + $this->out('', 1, Shell::QUIET); + $this->out(__d('cake_console', 'Bake All complete'), 1, Shell::QUIET); + array_shift($this->args); + } else { + $this->error(__d('cake_console', 'Bake All could not continue without a valid model')); + } + return $this->_stop(); + } + + /** + * Gets the option parser instance and configures it. + * + * @return ConsoleOptionParser + */ + public function getOptionParser() + { + $parser = parent::getOptionParser(); + + $parser->description( + __d('cake_console', 'The Bake script generates controllers, views and models for your application.' . + ' If run with no command line arguments, Bake guides the user through the class creation process.' . + ' You can customize the generation process by telling Bake where different parts of your application are using command line arguments.') + )->addSubcommand('all', [ + 'help' => __d('cake_console', 'Bake a complete MVC. optional of a Model') + ])->addSubcommand('project', [ + 'help' => __d('cake_console', 'Bake a new app folder in the path supplied or in current directory if no path is specified'), + 'parser' => $this->Project->getOptionParser() + ])->addSubcommand('plugin', [ + 'help' => __d('cake_console', 'Bake a new plugin folder in the path supplied or in current directory if no path is specified.'), + 'parser' => $this->Plugin->getOptionParser() + ])->addSubcommand('db_config', [ + 'help' => __d('cake_console', 'Bake a database.php file in config directory.'), + 'parser' => $this->DbConfig->getOptionParser() + ])->addSubcommand('model', [ + 'help' => __d('cake_console', 'Bake a model.'), + 'parser' => $this->Model->getOptionParser() + ])->addSubcommand('view', [ + 'help' => __d('cake_console', 'Bake views for controllers.'), + 'parser' => $this->View->getOptionParser() + ])->addSubcommand('controller', [ + 'help' => __d('cake_console', 'Bake a controller.'), + 'parser' => $this->Controller->getOptionParser() + ])->addSubcommand('fixture', [ + 'help' => __d('cake_console', 'Bake a fixture.'), + 'parser' => $this->Fixture->getOptionParser() + ])->addSubcommand('test', [ + 'help' => __d('cake_console', 'Bake a unit test.'), + 'parser' => $this->Test->getOptionParser() + ])->addOption('connection', [ + 'help' => __d('cake_console', 'Database connection to use in conjunction with `bake all`.'), + 'short' => 'c', + 'default' => 'default' + ])->addOption('theme', [ + 'short' => 't', + 'help' => __d('cake_console', 'Theme to use when baking code.') + ]); + + return $parser; + } } diff --git a/lib/Cake/Console/Command/CommandListShell.php b/lib/Cake/Console/Command/CommandListShell.php index 49b98778..b12817c0 100755 --- a/lib/Cake/Console/Command/CommandListShell.php +++ b/lib/Cake/Console/Command/CommandListShell.php @@ -22,123 +22,129 @@ * * @package Cake.Console.Command */ -class CommandListShell extends AppShell { - -/** - * Contains tasks to load and instantiate - * - * @var array - */ - public $tasks = array('Command'); - -/** - * startup - * - * @return void - */ - public function startup() { - if (empty($this->params['xml'])) { - parent::startup(); - } - } - -/** - * Main function Prints out the list of shells. - * - * @return void - */ - public function main() { - if (empty($this->params['xml'])) { - $this->out(__d('cake_console', "Current Paths:"), 2); - $this->out(" -app: " . APP_DIR); - $this->out(" -working: " . rtrim(APP, DS)); - $this->out(" -root: " . rtrim(ROOT, DS)); - $this->out(" -core: " . rtrim(CORE_PATH, DS)); - $this->out(" -webroot: " . rtrim(WWW_ROOT, DS)); - $this->out(""); - $this->out(__d('cake_console', "Changing Paths:"), 2); - $this->out(__d('cake_console', "Your working path should be the same as your application path. To change your path use the '-app' param.")); - $this->out(__d('cake_console', "Example: %s or %s", '-app relative/path/to/myapp', '-app /absolute/path/to/myapp'), 2); - - $this->out(__d('cake_console', "Available Shells:"), 2); - } - - $shellList = $this->Command->getShellList(); - if (empty($shellList)) { - return; - } - - if (empty($this->params['xml'])) { - $this->_asText($shellList); - } else { - $this->_asXml($shellList); - } - } - -/** - * Output text. - * - * @param array $shellList The shell list. - * @return void - */ - protected function _asText($shellList) { - foreach ($shellList as $plugin => $commands) { - sort($commands); - $this->out(sprintf('[%s] %s', $plugin, implode(', ', $commands))); - $this->out(); - } - - $this->out(__d('cake_console', "To run an app or core command, type cake shell_name [args]")); - $this->out(__d('cake_console', "To run a plugin command, type cake Plugin.shell_name [args]")); - $this->out(__d('cake_console', "To get help on a specific command, type cake shell_name --help"), 2); - } - -/** - * Output as XML - * - * @param array $shellList The shell list. - * @return void - */ - protected function _asXml($shellList) { - $plugins = CakePlugin::loaded(); - $shells = new SimpleXmlElement(''); - foreach ($shellList as $plugin => $commands) { - foreach ($commands as $command) { - $callable = $command; - if (in_array($plugin, $plugins)) { - $callable = Inflector::camelize($plugin) . '.' . $command; - } - - $shell = $shells->addChild('shell'); - $shell->addAttribute('name', $command); - $shell->addAttribute('call_as', $callable); - $shell->addAttribute('provider', $plugin); - $shell->addAttribute('help', $callable . ' -h'); - } - } - $this->stdout->outputAs(ConsoleOutput::RAW); - $this->out($shells->saveXml()); - } - -/** - * Gets the option parser instance and configures it. - * - * @return ConsoleOptionParser - */ - public function getOptionParser() { - $parser = parent::getOptionParser(); - - $parser->description( - __d('cake_console', 'Get the list of available shells for this CakePHP application.') - )->addOption('sort', array( - 'help' => __d('cake_console', 'Does nothing (deprecated)'), - 'boolean' => true - ))->addOption('xml', array( - 'help' => __d('cake_console', 'Get the listing as XML.'), - 'boolean' => true - )); - - return $parser; - } +class CommandListShell extends AppShell +{ + + /** + * Contains tasks to load and instantiate + * + * @var array + */ + public $tasks = ['Command']; + + /** + * startup + * + * @return void + */ + public function startup() + { + if (empty($this->params['xml'])) { + parent::startup(); + } + } + + /** + * Main function Prints out the list of shells. + * + * @return void + */ + public function main() + { + if (empty($this->params['xml'])) { + $this->out(__d('cake_console', "Current Paths:"), 2); + $this->out(" -app: " . APP_DIR); + $this->out(" -working: " . rtrim(APP, DS)); + $this->out(" -root: " . rtrim(ROOT, DS)); + $this->out(" -core: " . rtrim(CORE_PATH, DS)); + $this->out(" -webroot: " . rtrim(WWW_ROOT, DS)); + $this->out(""); + $this->out(__d('cake_console', "Changing Paths:"), 2); + $this->out(__d('cake_console', "Your working path should be the same as your application path. To change your path use the '-app' param.")); + $this->out(__d('cake_console', "Example: %s or %s", '-app relative/path/to/myapp', '-app /absolute/path/to/myapp'), 2); + + $this->out(__d('cake_console', "Available Shells:"), 2); + } + + $shellList = $this->Command->getShellList(); + if (empty($shellList)) { + return; + } + + if (empty($this->params['xml'])) { + $this->_asText($shellList); + } else { + $this->_asXml($shellList); + } + } + + /** + * Output text. + * + * @param array $shellList The shell list. + * @return void + */ + protected function _asText($shellList) + { + foreach ($shellList as $plugin => $commands) { + sort($commands); + $this->out(sprintf('[%s] %s', $plugin, implode(', ', $commands))); + $this->out(); + } + + $this->out(__d('cake_console', "To run an app or core command, type cake shell_name [args]")); + $this->out(__d('cake_console', "To run a plugin command, type cake Plugin.shell_name [args]")); + $this->out(__d('cake_console', "To get help on a specific command, type cake shell_name --help"), 2); + } + + /** + * Output as XML + * + * @param array $shellList The shell list. + * @return void + */ + protected function _asXml($shellList) + { + $plugins = CakePlugin::loaded(); + $shells = new SimpleXmlElement(''); + foreach ($shellList as $plugin => $commands) { + foreach ($commands as $command) { + $callable = $command; + if (in_array($plugin, $plugins)) { + $callable = Inflector::camelize($plugin) . '.' . $command; + } + + $shell = $shells->addChild('shell'); + $shell->addAttribute('name', $command); + $shell->addAttribute('call_as', $callable); + $shell->addAttribute('provider', $plugin); + $shell->addAttribute('help', $callable . ' -h'); + } + } + $this->stdout->outputAs(ConsoleOutput::RAW); + $this->out($shells->saveXml()); + } + + /** + * Gets the option parser instance and configures it. + * + * @return ConsoleOptionParser + */ + public function getOptionParser() + { + $parser = parent::getOptionParser(); + + $parser->description( + __d('cake_console', 'Get the list of available shells for this CakePHP application.') + )->addOption('sort', [ + 'help' => __d('cake_console', 'Does nothing (deprecated)'), + 'boolean' => true + ])->addOption('xml', [ + 'help' => __d('cake_console', 'Get the listing as XML.'), + 'boolean' => true + ]); + + return $parser; + } } diff --git a/lib/Cake/Console/Command/CompletionShell.php b/lib/Cake/Console/Command/CompletionShell.php index ac7ffeef..d8d6b44c 100755 --- a/lib/Cake/Console/Command/CompletionShell.php +++ b/lib/Cake/Console/Command/CompletionShell.php @@ -21,21 +21,23 @@ * * @package Cake.Console.Command */ -class CompletionShell extends AppShell { +class CompletionShell extends AppShell +{ /** * Contains tasks to load and instantiate * * @var array */ - public $tasks = array('Command'); + public $tasks = ['Command']; /** * Echo no header by overriding the startup method * * @return void */ - public function startup() { + public function startup() + { } /** @@ -43,26 +45,89 @@ public function startup() { * * @return void */ - public function main() { + public function main() + { return $this->out($this->getOptionParser()->help()); } + /** + * Gets the option parser instance and configures it. + * + * @return ConsoleOptionParser + */ + public function getOptionParser() + { + $parser = parent::getOptionParser(); + + $parser->description( + __d('cake_console', 'Used by shells like bash to autocomplete command name, options and arguments') + )->addSubcommand('commands', [ + 'help' => __d('cake_console', 'Output a list of available commands'), + 'parser' => [ + 'description' => __d('cake_console', 'List all availables'), + 'arguments' => [ + ] + ] + ])->addSubcommand('subcommands', [ + 'help' => __d('cake_console', 'Output a list of available subcommands'), + 'parser' => [ + 'description' => __d('cake_console', 'List subcommands for a command'), + 'arguments' => [ + 'command' => [ + 'help' => __d('cake_console', 'The command name'), + 'required' => true, + ] + ] + ] + ])->addSubcommand('options', [ + 'help' => __d('cake_console', 'Output a list of available options'), + 'parser' => [ + 'description' => __d('cake_console', 'List options'), + 'arguments' => [ + 'command' => [ + 'help' => __d('cake_console', 'The command name'), + 'required' => false, + ] + ] + ] + ])->epilog( + __d('cake_console', 'This command is not intended to be called manually') + ); + + return $parser; + } + /** * list commands * * @return void */ - public function commands() { + public function commands() + { $options = $this->Command->commands(); return $this->_output($options); } + /** + * Emit results as a string, space delimited + * + * @param array $options The options to output + * @return void + */ + protected function _output($options = []) + { + if ($options) { + return $this->out(implode(' ', $options)); + } + } + /** * list options for the named command * * @return void */ - public function options() { + public function options() + { $commandName = ''; if (!empty($this->args[0])) { $commandName = $this->args[0]; @@ -77,7 +142,8 @@ public function options() { * * @return void */ - public function subCommands() { + public function subCommands() + { if (!$this->args) { return $this->_output(); } @@ -91,65 +157,8 @@ public function subCommands() { * * @return void */ - public function fuzzy() { + public function fuzzy() + { return $this->_output(); } - - /** - * Gets the option parser instance and configures it. - * - * @return ConsoleOptionParser - */ - public function getOptionParser() { - $parser = parent::getOptionParser(); - - $parser->description( - __d('cake_console', 'Used by shells like bash to autocomplete command name, options and arguments') - )->addSubcommand('commands', array( - 'help' => __d('cake_console', 'Output a list of available commands'), - 'parser' => array( - 'description' => __d('cake_console', 'List all availables'), - 'arguments' => array( - ) - ) - ))->addSubcommand('subcommands', array( - 'help' => __d('cake_console', 'Output a list of available subcommands'), - 'parser' => array( - 'description' => __d('cake_console', 'List subcommands for a command'), - 'arguments' => array( - 'command' => array( - 'help' => __d('cake_console', 'The command name'), - 'required' => true, - ) - ) - ) - ))->addSubcommand('options', array( - 'help' => __d('cake_console', 'Output a list of available options'), - 'parser' => array( - 'description' => __d('cake_console', 'List options'), - 'arguments' => array( - 'command' => array( - 'help' => __d('cake_console', 'The command name'), - 'required' => false, - ) - ) - ) - ))->epilog( - __d('cake_console', 'This command is not intended to be called manually') - ); - - return $parser; - } - - /** - * Emit results as a string, space delimited - * - * @param array $options The options to output - * @return void - */ - protected function _output($options = array()) { - if ($options) { - return $this->out(implode(' ', $options)); - } - } } \ No newline at end of file diff --git a/lib/Cake/Console/Command/ConsoleShell.php b/lib/Cake/Console/Command/ConsoleShell.php index e157ff86..6c47c449 100755 --- a/lib/Cake/Console/Command/ConsoleShell.php +++ b/lib/Cake/Console/Command/ConsoleShell.php @@ -21,494 +21,514 @@ * @package Cake.Console.Command * @deprecated 3.0.0 Deprecated since version 2.4, will be removed in 3.0 */ -class ConsoleShell extends AppShell { - -/** - * Available binding types - * - * @var array - */ - public $associations = array('hasOne', 'hasMany', 'belongsTo', 'hasAndBelongsToMany'); - -/** - * Chars that describe invalid commands - * - * @var array - */ - public $badCommandChars = array('$', ';'); - -/** - * Available models - * - * @var array - */ - public $models = array(); - -/** - * _finished - * - * This shell is perpetual, setting this property to true exits the process - * - * @var mixed - */ - protected $_finished = false; - -/** - * _methodPatterns - * - * @var array - */ - protected $_methodPatterns = array( - 'help' => '/^(help|\?)/', - '_exit' => '/^(quit|exit)/', - '_models' => '/^models/i', - '_bind' => '/^(\w+) bind (\w+) (\w+)/', - '_unbind' => '/^(\w+) unbind (\w+) (\w+)/', - '_find' => '/.+->find/', - '_save' => '/.+->save/', - '_columns' => '/^(\w+) columns/', - '_routesReload' => '/^routes\s+reload/i', - '_routesShow' => '/^routes\s+show/i', - '_routeToString' => '/^route\s+(\(.*\))$/i', - '_routeToArray' => '/^route\s+(.*)$/i', - ); - -/** - * Override startup of the Shell - * - * @return void - */ - public function startup() { - App::uses('Dispatcher', 'Routing'); - $this->Dispatcher = new Dispatcher(); - $this->models = App::objects('Model'); - - foreach ($this->models as $model) { - $class = $model; - App::uses($class, 'Model'); - $this->{$class} = new $class(); - } - $this->out(__d('cake_console', 'Model classes:')); - $this->hr(); - - foreach ($this->models as $model) { - $this->out(" - {$model}"); - } - - if (!$this->_loadRoutes()) { - $message = __d( - 'cake_console', - 'There was an error loading the routes config. Please check that the file exists and contains no errors.' - ); - $this->err($message); - } - } - -/** - * Gets the option parser instance and configures it. - * - * @return ConsoleOptionParser - */ - public function getOptionParser() { - $parser = parent::getOptionParser(); - - $parser->description(array( - 'The interactive console is a tool for testing parts of your', - 'app before you write code.', - '', - 'See below for a list of supported commands.' - ))->epilog(array( - 'Model testing', - '', - 'To test model results, use the name of your model without a leading $', - 'e.g. Foo->find("all")', - "", - 'To dynamically set associations, you can do the following:', - '', - "\tModelA bind ModelB", - '', - "where the supported associations are hasOne, hasMany, belongsTo, hasAndBelongsToMany", - "", - 'To dynamically remove associations, you can do the following:', - '', - "\t ModelA unbind ModelB", - '', - "where the supported associations are the same as above", - "", - "To save a new field in a model, you can do the following:", - '', - "\tModelA->save(array('foo' => 'bar', 'baz' => 0))", - '', - "where you are passing a hash of data to be saved in the format", - "of field => value pairs", - "", - "To get column information for a model, use the following:", - '', - "\tModelA columns", - '', - "which returns a list of columns and their type", - "", - 'Route testing', - "", - 'To test URLs against your app\'s route configuration, type:', - "", - "\tRoute ", - "", - "where url is the path to your your action plus any query parameters,", - "minus the application's base path. For example:", - "", - "\tRoute /posts/view/1", - "", - "will return something like the following:", - "", - "\tarray(", - "\t [...]", - "\t 'controller' => 'posts',", - "\t 'action' => 'view',", - "\t [...]", - "\t)", - "", - 'Alternatively, you can use simple array syntax to test reverse', - 'To reload your routes config (Config/routes.php), do the following:', - "", - "\tRoutes reload", - "", - 'To show all connected routes, do the following:', - '', - "\tRoutes show", - )); - - return $parser; - } -/** - * Prints the help message - * - * @return void - */ - public function help() { - $optionParser = $this->getOptionParser(); - $this->out($optionParser->epilog()); - } - -/** - * Override main() to handle action - * - * @param string $command The command to run. - * @return void - */ - public function main($command = null) { - $this->_finished = false; - while (!$this->_finished) { - if (empty($command)) { - $command = trim($this->in('')); - } - - $method = $this->_method($command); - - if ($method) { - $this->$method($command); - } else { - $this->out(__d('cake_console', "Invalid command")); - $this->out(); - } - $command = ''; - } - } - -/** - * Determine the method to process the current command - * - * @param string $command The command to run. - * @return string or false - */ - protected function _method($command) { - foreach ($this->_methodPatterns as $method => $pattern) { - if (preg_match($pattern, $command)) { - return $method; - } - } - - return false; - } - -/** - * Set the finiished property so that the loop in main method ends - * - * @return void - */ - protected function _exit() { - $this->_finished = true; - } - -/** - * List all models - * - * @return void - */ - protected function _models() { - $this->out(__d('cake_console', 'Model classes:')); - $this->hr(); - foreach ($this->models as $model) { - $this->out(" - {$model}"); - } - } - -/** - * Bind an association - * - * @param mixed $command The command to run. - * @return void - */ - protected function _bind($command) { - preg_match($this->_methodPatterns[__FUNCTION__], $command, $tmp); - - foreach ($tmp as $data) { - $data = strip_tags($data); - $data = str_replace($this->badCommandChars, "", $data); - } - - $modelA = $tmp[1]; - $association = $tmp[2]; - $modelB = $tmp[3]; - - if ($this->_isValidModel($modelA) && $this->_isValidModel($modelB) && in_array($association, $this->associations)) { - $this->{$modelA}->bindModel(array($association => array($modelB => array('className' => $modelB))), false); - $this->out(__d('cake_console', "Created %s association between %s and %s", - $association, $modelA, $modelB)); - } else { - $this->out(__d('cake_console', "Please verify you are using valid models and association types")); - } - } - -/** - * Unbind an association - * - * @param mixed $command The command to run. - * @return void - */ - protected function _unbind($command) { - preg_match($this->_methodPatterns[__FUNCTION__], $command, $tmp); - - foreach ($tmp as $data) { - $data = strip_tags($data); - $data = str_replace($this->badCommandChars, "", $data); - } - - $modelA = $tmp[1]; - $association = $tmp[2]; - $modelB = $tmp[3]; - - // Verify that there is actually an association to unbind - $currentAssociations = $this->{$modelA}->getAssociated(); - $validCurrentAssociation = false; - - foreach ($currentAssociations as $model => $currentAssociation) { - if ($model === $modelB && $association === $currentAssociation) { - $validCurrentAssociation = true; - } - } - - if ($this->_isValidModel($modelA) && $this->_isValidModel($modelB) && in_array($association, $this->associations) && $validCurrentAssociation) { - $this->{$modelA}->unbindModel(array($association => array($modelB))); - $this->out(__d('cake_console', "Removed %s association between %s and %s", - $association, $modelA, $modelB)); - } else { - $this->out(__d('cake_console', "Please verify you are using valid models, valid current association, and valid association types")); - } - } - -/** - * Perform a find - * - * @param mixed $command The command to run. - * @return void - */ - protected function _find($command) { - $command = strip_tags($command); - $command = str_replace($this->badCommandChars, "", $command); - - // Do we have a valid model? - list($modelToCheck) = explode('->', $command); - - if ($this->_isValidModel($modelToCheck)) { - $findCommand = "\$data = \$this->$command;"; - //@codingStandardsIgnoreStart - @eval($findCommand); - //@codingStandardsIgnoreEnd - - if (is_array($data)) { - foreach ($data as $idx => $results) { - if (is_numeric($idx)) { // findAll() output - foreach ($results as $modelName => $result) { - $this->out("$modelName"); - - foreach ($result as $field => $value) { - if (is_array($value)) { - foreach ($value as $field2 => $value2) { - $this->out("\t$field2: $value2"); - } - - $this->out(); - } else { - $this->out("\t$field: $value"); - } - } - } - } else { // find() output - $this->out($idx); - - foreach ($results as $field => $value) { - if (is_array($value)) { - foreach ($value as $field2 => $value2) { - $this->out("\t$field2: $value2"); - } - - $this->out(); - } else { - $this->out("\t$field: $value"); - } - } - } - } - } else { - $this->out(); - $this->out(__d('cake_console', "No result set found")); - } - } else { - $this->out(__d('cake_console', "%s is not a valid model", $modelToCheck)); - } - } - -/** - * Save a record - * - * @param mixed $command The command to run. - * @return void - */ - protected function _save($command) { - // Validate the model we're trying to save here - $command = strip_tags($command); - $command = str_replace($this->badCommandChars, "", $command); - list($modelToSave) = explode("->", $command); - - if ($this->_isValidModel($modelToSave)) { - // Extract the array of data we are trying to build - list(, $data) = explode("->save", $command); - $data = preg_replace('/^\(*(array)?\(*(.+?)\)*$/i', '\\2', $data); - $saveCommand = "\$this->{$modelToSave}->save(array('{$modelToSave}' => array({$data})));"; - //@codingStandardsIgnoreStart - @eval($saveCommand); - //@codingStandardsIgnoreEnd - $this->out(__d('cake_console', 'Saved record for %s', $modelToSave)); - } - } - -/** - * Show the columns for a model - * - * @param mixed $command The command to run. - * @return void - */ - protected function _columns($command) { - preg_match($this->_methodPatterns[__FUNCTION__], $command, $tmp); - - $modelToCheck = strip_tags(str_replace($this->badCommandChars, "", $tmp[1])); - - if ($this->_isValidModel($modelToCheck)) { - // Get the column info for this model - $fieldsCommand = "\$data = \$this->{$modelToCheck}->getColumnTypes();"; - //@codingStandardsIgnoreStart - @eval($fieldsCommand); - //@codingStandardsIgnoreEnd - - if (is_array($data)) { - foreach ($data as $field => $type) { - $this->out("\t{$field}: {$type}"); - } - } - } else { - $this->out(__d('cake_console', "Please verify that you selected a valid model")); - } - } - -/** - * Reload route definitions - * - * @return void - */ - protected function _routesReload() { - if (!$this->_loadRoutes()) { - return $this->err(__d('cake_console', "There was an error loading the routes config. Please check that the file exists and is free of parse errors.")); - } - $this->out(__d('cake_console', "Routes configuration reloaded, %d routes connected", count(Router::$routes))); - } - -/** - * Show all routes - * - * @return void - */ - protected function _routesShow() { - $this->out(print_r(Hash::combine(Router::$routes, '{n}.template', '{n}.defaults'), true)); - } - -/** - * Parse an array URL and show the equivalent URL as a string - * - * @param mixed $command The command to run. - * @return void - */ - protected function _routeToString($command) { - preg_match($this->_methodPatterns[__FUNCTION__], $command, $tmp); - - //@codingStandardsIgnoreStart - if ($url = eval('return array' . $tmp[1] . ';')) { - //@codingStandardsIgnoreEnd - $this->out(Router::url($url)); - } - } - -/** - * Parse a string URL and show as an array - * - * @param mixed $command The command to run. - * @return void - */ - protected function _routeToArray($command) { - preg_match($this->_methodPatterns[__FUNCTION__], $command, $tmp); - - $this->out(var_export(Router::parse($tmp[1]), true)); - } - -/** - * Tells if the specified model is included in the list of available models - * - * @param string $modelToCheck The model to check. - * @return bool true if is an available model, false otherwise - */ - protected function _isValidModel($modelToCheck) { - return in_array($modelToCheck, $this->models); - } - -/** - * Reloads the routes configuration from app/Config/routes.php, and compiles - * all routes found - * - * @return bool True if config reload was a success, otherwise false - */ - protected function _loadRoutes() { - Router::reload(); - extract(Router::getNamedExpressions()); - - //@codingStandardsIgnoreStart - if (!@include CONFIG . 'routes.php') { - //@codingStandardsIgnoreEnd - return false; - } - CakePlugin::routes(); - - Router::parse('/'); - return true; - } +class ConsoleShell extends AppShell +{ + + /** + * Available binding types + * + * @var array + */ + public $associations = ['hasOne', 'hasMany', 'belongsTo', 'hasAndBelongsToMany']; + + /** + * Chars that describe invalid commands + * + * @var array + */ + public $badCommandChars = ['$', ';']; + + /** + * Available models + * + * @var array + */ + public $models = []; + + /** + * _finished + * + * This shell is perpetual, setting this property to true exits the process + * + * @var mixed + */ + protected $_finished = false; + + /** + * _methodPatterns + * + * @var array + */ + protected $_methodPatterns = [ + 'help' => '/^(help|\?)/', + '_exit' => '/^(quit|exit)/', + '_models' => '/^models/i', + '_bind' => '/^(\w+) bind (\w+) (\w+)/', + '_unbind' => '/^(\w+) unbind (\w+) (\w+)/', + '_find' => '/.+->find/', + '_save' => '/.+->save/', + '_columns' => '/^(\w+) columns/', + '_routesReload' => '/^routes\s+reload/i', + '_routesShow' => '/^routes\s+show/i', + '_routeToString' => '/^route\s+(\(.*\))$/i', + '_routeToArray' => '/^route\s+(.*)$/i', + ]; + + /** + * Override startup of the Shell + * + * @return void + */ + public function startup() + { + App::uses('Dispatcher', 'Routing'); + $this->Dispatcher = new Dispatcher(); + $this->models = App::objects('Model'); + + foreach ($this->models as $model) { + $class = $model; + App::uses($class, 'Model'); + $this->{$class} = new $class(); + } + $this->out(__d('cake_console', 'Model classes:')); + $this->hr(); + + foreach ($this->models as $model) { + $this->out(" - {$model}"); + } + + if (!$this->_loadRoutes()) { + $message = __d( + 'cake_console', + 'There was an error loading the routes config. Please check that the file exists and contains no errors.' + ); + $this->err($message); + } + } + + /** + * Reloads the routes configuration from app/Config/routes.php, and compiles + * all routes found + * + * @return bool True if config reload was a success, otherwise false + */ + protected function _loadRoutes() + { + Router::reload(); + extract(Router::getNamedExpressions()); + + //@codingStandardsIgnoreStart + if (!@include CONFIG . 'routes.php') { + //@codingStandardsIgnoreEnd + return false; + } + CakePlugin::routes(); + + Router::parse('/'); + return true; + } + + /** + * Prints the help message + * + * @return void + */ + public function help() + { + $optionParser = $this->getOptionParser(); + $this->out($optionParser->epilog()); + } + + /** + * Gets the option parser instance and configures it. + * + * @return ConsoleOptionParser + */ + public function getOptionParser() + { + $parser = parent::getOptionParser(); + + $parser->description([ + 'The interactive console is a tool for testing parts of your', + 'app before you write code.', + '', + 'See below for a list of supported commands.' + ])->epilog([ + 'Model testing', + '', + 'To test model results, use the name of your model without a leading $', + 'e.g. Foo->find("all")', + "", + 'To dynamically set associations, you can do the following:', + '', + "\tModelA bind ModelB", + '', + "where the supported associations are hasOne, hasMany, belongsTo, hasAndBelongsToMany", + "", + 'To dynamically remove associations, you can do the following:', + '', + "\t ModelA unbind ModelB", + '', + "where the supported associations are the same as above", + "", + "To save a new field in a model, you can do the following:", + '', + "\tModelA->save(array('foo' => 'bar', 'baz' => 0))", + '', + "where you are passing a hash of data to be saved in the format", + "of field => value pairs", + "", + "To get column information for a model, use the following:", + '', + "\tModelA columns", + '', + "which returns a list of columns and their type", + "", + 'Route testing', + "", + 'To test URLs against your app\'s route configuration, type:', + "", + "\tRoute ", + "", + "where url is the path to your your action plus any query parameters,", + "minus the application's base path. For example:", + "", + "\tRoute /posts/view/1", + "", + "will return something like the following:", + "", + "\tarray(", + "\t [...]", + "\t 'controller' => 'posts',", + "\t 'action' => 'view',", + "\t [...]", + "\t)", + "", + 'Alternatively, you can use simple array syntax to test reverse', + 'To reload your routes config (Config/routes.php), do the following:', + "", + "\tRoutes reload", + "", + 'To show all connected routes, do the following:', + '', + "\tRoutes show", + ]); + + return $parser; + } + + /** + * Override main() to handle action + * + * @param string $command The command to run. + * @return void + */ + public function main($command = null) + { + $this->_finished = false; + while (!$this->_finished) { + if (empty($command)) { + $command = trim($this->in('')); + } + + $method = $this->_method($command); + + if ($method) { + $this->$method($command); + } else { + $this->out(__d('cake_console', "Invalid command")); + $this->out(); + } + $command = ''; + } + } + + /** + * Determine the method to process the current command + * + * @param string $command The command to run. + * @return string or false + */ + protected function _method($command) + { + foreach ($this->_methodPatterns as $method => $pattern) { + if (preg_match($pattern, $command)) { + return $method; + } + } + + return false; + } + + /** + * Set the finiished property so that the loop in main method ends + * + * @return void + */ + protected function _exit() + { + $this->_finished = true; + } + + /** + * List all models + * + * @return void + */ + protected function _models() + { + $this->out(__d('cake_console', 'Model classes:')); + $this->hr(); + foreach ($this->models as $model) { + $this->out(" - {$model}"); + } + } + + /** + * Bind an association + * + * @param mixed $command The command to run. + * @return void + */ + protected function _bind($command) + { + preg_match($this->_methodPatterns[__FUNCTION__], $command, $tmp); + + foreach ($tmp as $data) { + $data = strip_tags($data); + $data = str_replace($this->badCommandChars, "", $data); + } + + $modelA = $tmp[1]; + $association = $tmp[2]; + $modelB = $tmp[3]; + + if ($this->_isValidModel($modelA) && $this->_isValidModel($modelB) && in_array($association, $this->associations)) { + $this->{$modelA}->bindModel([$association => [$modelB => ['className' => $modelB]]], false); + $this->out(__d('cake_console', "Created %s association between %s and %s", + $association, $modelA, $modelB)); + } else { + $this->out(__d('cake_console', "Please verify you are using valid models and association types")); + } + } + + /** + * Tells if the specified model is included in the list of available models + * + * @param string $modelToCheck The model to check. + * @return bool true if is an available model, false otherwise + */ + protected function _isValidModel($modelToCheck) + { + return in_array($modelToCheck, $this->models); + } + + /** + * Unbind an association + * + * @param mixed $command The command to run. + * @return void + */ + protected function _unbind($command) + { + preg_match($this->_methodPatterns[__FUNCTION__], $command, $tmp); + + foreach ($tmp as $data) { + $data = strip_tags($data); + $data = str_replace($this->badCommandChars, "", $data); + } + + $modelA = $tmp[1]; + $association = $tmp[2]; + $modelB = $tmp[3]; + + // Verify that there is actually an association to unbind + $currentAssociations = $this->{$modelA}->getAssociated(); + $validCurrentAssociation = false; + + foreach ($currentAssociations as $model => $currentAssociation) { + if ($model === $modelB && $association === $currentAssociation) { + $validCurrentAssociation = true; + } + } + + if ($this->_isValidModel($modelA) && $this->_isValidModel($modelB) && in_array($association, $this->associations) && $validCurrentAssociation) { + $this->{$modelA}->unbindModel([$association => [$modelB]]); + $this->out(__d('cake_console', "Removed %s association between %s and %s", + $association, $modelA, $modelB)); + } else { + $this->out(__d('cake_console', "Please verify you are using valid models, valid current association, and valid association types")); + } + } + + /** + * Perform a find + * + * @param mixed $command The command to run. + * @return void + */ + protected function _find($command) + { + $command = strip_tags($command); + $command = str_replace($this->badCommandChars, "", $command); + + // Do we have a valid model? + list($modelToCheck) = explode('->', $command); + + if ($this->_isValidModel($modelToCheck)) { + $findCommand = "\$data = \$this->$command;"; + //@codingStandardsIgnoreStart + @eval($findCommand); + //@codingStandardsIgnoreEnd + + if (is_array($data)) { + foreach ($data as $idx => $results) { + if (is_numeric($idx)) { // findAll() output + foreach ($results as $modelName => $result) { + $this->out("$modelName"); + + foreach ($result as $field => $value) { + if (is_array($value)) { + foreach ($value as $field2 => $value2) { + $this->out("\t$field2: $value2"); + } + + $this->out(); + } else { + $this->out("\t$field: $value"); + } + } + } + } else { // find() output + $this->out($idx); + + foreach ($results as $field => $value) { + if (is_array($value)) { + foreach ($value as $field2 => $value2) { + $this->out("\t$field2: $value2"); + } + + $this->out(); + } else { + $this->out("\t$field: $value"); + } + } + } + } + } else { + $this->out(); + $this->out(__d('cake_console', "No result set found")); + } + } else { + $this->out(__d('cake_console', "%s is not a valid model", $modelToCheck)); + } + } + + /** + * Save a record + * + * @param mixed $command The command to run. + * @return void + */ + protected function _save($command) + { + // Validate the model we're trying to save here + $command = strip_tags($command); + $command = str_replace($this->badCommandChars, "", $command); + list($modelToSave) = explode("->", $command); + + if ($this->_isValidModel($modelToSave)) { + // Extract the array of data we are trying to build + list(, $data) = explode("->save", $command); + $data = preg_replace('/^\(*(array)?\(*(.+?)\)*$/i', '\\2', $data); + $saveCommand = "\$this->{$modelToSave}->save(array('{$modelToSave}' => array({$data})));"; + //@codingStandardsIgnoreStart + @eval($saveCommand); + //@codingStandardsIgnoreEnd + $this->out(__d('cake_console', 'Saved record for %s', $modelToSave)); + } + } + + /** + * Show the columns for a model + * + * @param mixed $command The command to run. + * @return void + */ + protected function _columns($command) + { + preg_match($this->_methodPatterns[__FUNCTION__], $command, $tmp); + + $modelToCheck = strip_tags(str_replace($this->badCommandChars, "", $tmp[1])); + + if ($this->_isValidModel($modelToCheck)) { + // Get the column info for this model + $fieldsCommand = "\$data = \$this->{$modelToCheck}->getColumnTypes();"; + //@codingStandardsIgnoreStart + @eval($fieldsCommand); + //@codingStandardsIgnoreEnd + + if (is_array($data)) { + foreach ($data as $field => $type) { + $this->out("\t{$field}: {$type}"); + } + } + } else { + $this->out(__d('cake_console', "Please verify that you selected a valid model")); + } + } + + /** + * Reload route definitions + * + * @return void + */ + protected function _routesReload() + { + if (!$this->_loadRoutes()) { + return $this->err(__d('cake_console', "There was an error loading the routes config. Please check that the file exists and is free of parse errors.")); + } + $this->out(__d('cake_console', "Routes configuration reloaded, %d routes connected", count(Router::$routes))); + } + + /** + * Show all routes + * + * @return void + */ + protected function _routesShow() + { + $this->out(print_r(Hash::combine(Router::$routes, '{n}.template', '{n}.defaults'), true)); + } + + /** + * Parse an array URL and show the equivalent URL as a string + * + * @param mixed $command The command to run. + * @return void + */ + protected function _routeToString($command) + { + preg_match($this->_methodPatterns[__FUNCTION__], $command, $tmp); + + //@codingStandardsIgnoreStart + if ($url = eval('return array' . $tmp[1] . ';')) { + //@codingStandardsIgnoreEnd + $this->out(Router::url($url)); + } + } + + /** + * Parse a string URL and show as an array + * + * @param mixed $command The command to run. + * @return void + */ + protected function _routeToArray($command) + { + preg_match($this->_methodPatterns[__FUNCTION__], $command, $tmp); + + $this->out(var_export(Router::parse($tmp[1]), true)); + } } diff --git a/lib/Cake/Console/Command/I18nShell.php b/lib/Cake/Console/Command/I18nShell.php index 79344a9b..cc5aa125 100755 --- a/lib/Cake/Console/Command/I18nShell.php +++ b/lib/Cake/Console/Command/I18nShell.php @@ -22,101 +22,106 @@ * * @package Cake.Console.Command */ -class I18nShell extends AppShell { +class I18nShell extends AppShell +{ -/** - * Contains database source to use - * - * @var string - */ - public $dataSource = 'default'; + /** + * Contains database source to use + * + * @var string + */ + public $dataSource = 'default'; -/** - * Contains tasks to load and instantiate - * - * @var array - */ - public $tasks = array('DbConfig', 'Extract'); + /** + * Contains tasks to load and instantiate + * + * @var array + */ + public $tasks = ['DbConfig', 'Extract']; -/** - * Override startup of the Shell - * - * @return mixed - */ - public function startup() { - $this->_welcome(); - if (isset($this->params['datasource'])) { - $this->dataSource = $this->params['datasource']; - } + /** + * Override startup of the Shell + * + * @return mixed + */ + public function startup() + { + $this->_welcome(); + if (isset($this->params['datasource'])) { + $this->dataSource = $this->params['datasource']; + } - if ($this->command && !in_array($this->command, array('help'))) { - if (!config('database')) { - $this->out(__d('cake_console', 'Your database configuration was not found. Take a moment to create one.')); - return $this->DbConfig->execute(); - } - } - } + if ($this->command && !in_array($this->command, ['help'])) { + if (!config('database')) { + $this->out(__d('cake_console', 'Your database configuration was not found. Take a moment to create one.')); + return $this->DbConfig->execute(); + } + } + } -/** - * Override main() for help message hook - * - * @return void - */ - public function main() { - $this->out(__d('cake_console', 'I18n Shell')); - $this->hr(); - $this->out(__d('cake_console', '[E]xtract POT file from sources')); - $this->out(__d('cake_console', '[I]nitialize i18n database table')); - $this->out(__d('cake_console', '[H]elp')); - $this->out(__d('cake_console', '[Q]uit')); + /** + * Override main() for help message hook + * + * @return void + */ + public function main() + { + $this->out(__d('cake_console', 'I18n Shell')); + $this->hr(); + $this->out(__d('cake_console', '[E]xtract POT file from sources')); + $this->out(__d('cake_console', '[I]nitialize i18n database table')); + $this->out(__d('cake_console', '[H]elp')); + $this->out(__d('cake_console', '[Q]uit')); - $choice = strtolower($this->in(__d('cake_console', 'What would you like to do?'), array('E', 'I', 'H', 'Q'))); - switch ($choice) { - case 'e': - $this->Extract->execute(); - break; - case 'i': - $this->initdb(); - break; - case 'h': - $this->out($this->OptionParser->help()); - break; - case 'q': - return $this->_stop(); - default: - $this->out(__d('cake_console', 'You have made an invalid selection. Please choose a command to execute by entering E, I, H, or Q.')); - } - $this->hr(); - $this->main(); - } + $choice = strtolower($this->in(__d('cake_console', 'What would you like to do?'), ['E', 'I', 'H', 'Q'])); + switch ($choice) { + case 'e': + $this->Extract->execute(); + break; + case 'i': + $this->initdb(); + break; + case 'h': + $this->out($this->OptionParser->help()); + break; + case 'q': + return $this->_stop(); + default: + $this->out(__d('cake_console', 'You have made an invalid selection. Please choose a command to execute by entering E, I, H, or Q.')); + } + $this->hr(); + $this->main(); + } -/** - * Initialize I18N database. - * - * @return void - */ - public function initdb() { - $this->dispatchShell('schema create i18n'); - } + /** + * Initialize I18N database. + * + * @return void + */ + public function initdb() + { + $this->dispatchShell('schema create i18n'); + } -/** - * Gets the option parser instance and configures it. - * - * @return ConsoleOptionParser - */ - public function getOptionParser() { - $parser = parent::getOptionParser(); + /** + * Gets the option parser instance and configures it. + * + * @return ConsoleOptionParser + */ + public function getOptionParser() + { + $parser = parent::getOptionParser(); - $parser->description( - __d('cake_console', 'I18n Shell initializes i18n database table for your application and generates .pot files(s) with translations.') - )->addSubcommand('initdb', array( - 'help' => __d('cake_console', 'Initialize the i18n table.') - ))->addSubcommand('extract', array( - 'help' => __d('cake_console', 'Extract the po translations from your application'), - 'parser' => $this->Extract->getOptionParser() - )); + $parser->description( + __d('cake_console', 'I18n Shell initializes i18n database table for your application and generates .pot files(s) with translations.') + )->addSubcommand('initdb', [ + 'help' => __d('cake_console', 'Initialize the i18n table.') + ])->addSubcommand('extract', [ + 'help' => __d('cake_console', 'Extract the po translations from your application'), + 'parser' => $this->Extract->getOptionParser() + ]); - return $parser; - } + return $parser; + } } diff --git a/lib/Cake/Console/Command/SchemaShell.php b/lib/Cake/Console/Command/SchemaShell.php index 46c7f810..7143151e 100755 --- a/lib/Cake/Console/Command/SchemaShell.php +++ b/lib/Cake/Console/Command/SchemaShell.php @@ -27,7 +27,8 @@ * @package Cake.Console.Command * @link https://book.cakephp.org/2.0/en/console-and-shells/schema-management-and-migrations.html */ -class SchemaShell extends AppShell { +class SchemaShell extends AppShell +{ /** * Schema class being used. @@ -48,7 +49,8 @@ class SchemaShell extends AppShell { * * @return void */ - public function startup() { + public function startup() + { $this->_welcome(); $this->out('Cake Schema Shell'); $this->hr(); @@ -58,7 +60,7 @@ public function startup() { $name = $path = $connection = $plugin = null; if (!empty($this->params['name'])) { $name = $this->params['name']; - } elseif (!empty($this->args[0]) && $this->args[0] !== 'snapshot') { + } else if (!empty($this->args[0]) && $this->args[0] !== 'snapshot') { $name = $this->params['name'] = $this->args[0]; } @@ -68,7 +70,7 @@ public function startup() { } if ($name && empty($this->params['file'])) { $this->params['file'] = Inflector::underscore($name); - } elseif (empty($this->params['file'])) { + } else if (empty($this->params['file'])) { $this->params['file'] = 'schema.php'; } if (strpos($this->params['file'], '.php') === false) { @@ -99,7 +101,8 @@ public function startup() { * * @return void */ - public function view() { + public function view() + { $File = new File($this->Schema->path . DS . $this->params['file']); if ($File->exists()) { $this->out($File->read()); @@ -116,12 +119,13 @@ public function view() { * * @return void */ - public function generate() { + public function generate() + { $this->out(__d('cake_console', 'Generating Schema...')); - $options = array(); + $options = []; if ($this->params['force']) { $options['models'] = false; - } elseif (!empty($this->params['models'])) { + } else if (!empty($this->params['models'])) { $options['models'] = CakeText::tokenize($this->params['models']); } @@ -133,7 +137,7 @@ public function generate() { if (!$snapshot && file_exists($this->Schema->path . DS . $this->params['file'])) { $snapshot = true; $prompt = __d('cake_console', "Schema file exists.\n [O]verwrite\n [S]napshot\n [Q]uit\nWould you like to do?"); - $result = strtolower($this->in($prompt, array('o', 's', 'q'), 's')); + $result = strtolower($this->in($prompt, ['o', 's', 'q'], 's')); if ($result === 'q') { return $this->_stop(); } @@ -202,7 +206,8 @@ public function generate() { * * @return string */ - public function dump() { + public function dump() + { $write = false; $Schema = $this->Schema->load(); if (!$Schema) { @@ -245,27 +250,19 @@ public function dump() { * * @return void */ - public function create() { + public function create() + { list($Schema, $table) = $this->_loadSchema(); $this->_create($Schema, $table); } - /** - * Run database create commands. Alias for run create. - * - * @return void - */ - public function update() { - list($Schema, $table) = $this->_loadSchema(); - $this->_update($Schema, $table); - } - /** * Prepares the Schema objects for database operations. * * @return void */ - protected function _loadSchema() { + protected function _loadSchema() + { $name = $plugin = null; if (!empty($this->params['name'])) { $name = $this->params['name']; @@ -279,11 +276,11 @@ protected function _loadSchema() { $this->out(__d('cake_console', 'Performing a dry run.')); } - $options = array( + $options = [ 'name' => $name, 'plugin' => $plugin, 'connection' => $this->params['connection'], - ); + ]; if (!empty($this->params['snapshot'])) { $fileName = basename($this->Schema->file, '.php'); $options['file'] = $fileName . '_' . $this->params['snapshot'] . '.php'; @@ -301,7 +298,7 @@ protected function _loadSchema() { if (isset($this->args[1])) { $table = $this->args[1]; } - return array(&$Schema, $table); + return [&$Schema, $table]; } /** @@ -312,17 +309,18 @@ protected function _loadSchema() { * @param string $table The table name. * @return void */ - protected function _create(CakeSchema $Schema, $table = null) { + protected function _create(CakeSchema $Schema, $table = null) + { $db = ConnectionManager::getDataSource($this->Schema->connection); - $drop = $create = array(); + $drop = $create = []; if (!$table) { foreach ($Schema->tables as $table => $fields) { $drop[$table] = $db->dropSchema($Schema, $table); $create[$table] = $db->createSchema($Schema, $table); } - } elseif (isset($Schema->tables[$table])) { + } else if (isset($Schema->tables[$table])) { $drop[$table] = $db->dropSchema($Schema, $table); $create[$table] = $db->createSchema($Schema, $table); } @@ -335,7 +333,7 @@ protected function _create(CakeSchema $Schema, $table = null) { $this->out(array_keys($drop)); if (!empty($this->params['yes']) || - $this->in(__d('cake_console', 'Are you sure you want to drop the table(s)?'), array('y', 'n'), 'n') === 'y' + $this->in(__d('cake_console', 'Are you sure you want to drop the table(s)?'), ['y', 'n'], 'n') === 'y' ) { $this->out(__d('cake_console', 'Dropping table(s).')); $this->_run($drop, 'drop', $Schema); @@ -345,7 +343,7 @@ protected function _create(CakeSchema $Schema, $table = null) { $this->out(array_keys($create)); if (!empty($this->params['yes']) || - $this->in(__d('cake_console', 'Are you sure you want to create the table(s)?'), array('y', 'n'), 'y') === 'y' + $this->in(__d('cake_console', 'Are you sure you want to create the table(s)?'), ['y', 'n'], 'y') === 'y' ) { $this->out(__d('cake_console', 'Creating table(s).')); $this->_run($create, 'create', $Schema); @@ -353,6 +351,64 @@ protected function _create(CakeSchema $Schema, $table = null) { $this->out(__d('cake_console', 'End create.')); } + /** + * Runs sql from _create() or _update() + * + * @param array $contents The contents to execute. + * @param string $event The event to fire + * @param CakeSchema $Schema The schema instance. + * @return void + */ + protected function _run($contents, $event, CakeSchema $Schema) + { + if (empty($contents)) { + $this->err(__d('cake_console', 'Sql could not be run')); + return; + } + Configure::write('debug', 2); + $db = ConnectionManager::getDataSource($this->Schema->connection); + + foreach ($contents as $table => $sql) { + if (empty($sql)) { + $this->out(__d('cake_console', '%s is up to date.', $table)); + } else { + if ($this->_dry === true) { + $this->out(__d('cake_console', 'Dry run for %s :', $table)); + $this->out($sql); + } else { + if (!$Schema->before([$event => $table])) { + return false; + } + $error = null; + try { + $db->execute($sql); + } catch (PDOException $e) { + $error = $table . ': ' . $e->getMessage(); + } + + $Schema->after([$event => $table, 'errors' => $error]); + + if (!empty($error)) { + $this->err($error); + } else { + $this->out(__d('cake_console', '%s updated.', $table)); + } + } + } + } + } + + /** + * Run database create commands. Alias for run create. + * + * @return void + */ + public function update() + { + list($Schema, $table) = $this->_loadSchema(); + $this->_update($Schema, $table); + } + /** * Update database with Schema object * Should be called via the run method @@ -361,32 +417,33 @@ protected function _create(CakeSchema $Schema, $table = null) { * @param string $table The table name. * @return void */ - protected function _update(&$Schema, $table = null) { + protected function _update(&$Schema, $table = null) + { $db = ConnectionManager::getDataSource($this->Schema->connection); $this->out(__d('cake_console', 'Comparing Database to Schema...')); - $options = array(); + $options = []; if (isset($this->params['force'])) { $options['models'] = false; } $Old = $this->Schema->read($options); $compare = $this->Schema->compare($Old, $Schema); - $contents = array(); + $contents = []; if (empty($table)) { foreach ($compare as $table => $changes) { if (isset($compare[$table]['create'])) { $contents[$table] = $db->createSchema($Schema, $table); } else { - $contents[$table] = $db->alterSchema(array($table => $compare[$table]), $table); + $contents[$table] = $db->alterSchema([$table => $compare[$table]], $table); } } - } elseif (isset($compare[$table])) { + } else if (isset($compare[$table])) { if (isset($compare[$table]['create'])) { $contents[$table] = $db->createSchema($Schema, $table); } else { - $contents[$table] = $db->alterSchema(array($table => $compare[$table]), $table); + $contents[$table] = $db->alterSchema([$table => $compare[$table]], $table); } } @@ -398,7 +455,7 @@ protected function _update(&$Schema, $table = null) { $this->out("\n" . __d('cake_console', 'The following statements will run.')); $this->out(array_map('trim', $contents)); if (!empty($this->params['yes']) || - $this->in(__d('cake_console', 'Are you sure you want to alter the tables?'), array('y', 'n'), 'n') === 'y' + $this->in(__d('cake_console', 'Are you sure you want to alter the tables?'), ['y', 'n'], 'n') === 'y' ) { $this->out(); $this->out(__d('cake_console', 'Updating Database...')); @@ -411,161 +468,116 @@ protected function _update(&$Schema, $table = null) { $this->out(__d('cake_console', 'End update.')); } - /** - * Runs sql from _create() or _update() - * - * @param array $contents The contents to execute. - * @param string $event The event to fire - * @param CakeSchema $Schema The schema instance. - * @return void - */ - protected function _run($contents, $event, CakeSchema $Schema) { - if (empty($contents)) { - $this->err(__d('cake_console', 'Sql could not be run')); - return; - } - Configure::write('debug', 2); - $db = ConnectionManager::getDataSource($this->Schema->connection); - - foreach ($contents as $table => $sql) { - if (empty($sql)) { - $this->out(__d('cake_console', '%s is up to date.', $table)); - } else { - if ($this->_dry === true) { - $this->out(__d('cake_console', 'Dry run for %s :', $table)); - $this->out($sql); - } else { - if (!$Schema->before(array($event => $table))) { - return false; - } - $error = null; - try { - $db->execute($sql); - } catch (PDOException $e) { - $error = $table . ': ' . $e->getMessage(); - } - - $Schema->after(array($event => $table, 'errors' => $error)); - - if (!empty($error)) { - $this->err($error); - } else { - $this->out(__d('cake_console', '%s updated.', $table)); - } - } - } - } - } - /** * Gets the option parser instance and configures it. * * @return ConsoleOptionParser */ - public function getOptionParser() { + public function getOptionParser() + { $parser = parent::getOptionParser(); - $plugin = array( + $plugin = [ 'short' => 'p', 'help' => __d('cake_console', 'The plugin to use.'), - ); - $connection = array( + ]; + $connection = [ 'short' => 'c', 'help' => __d('cake_console', 'Set the db config to use.'), 'default' => 'default' - ); - $path = array( + ]; + $path = [ 'help' => __d('cake_console', 'Path to read and write schema.php'), 'default' => CONFIG . 'Schema' - ); - $file = array( + ]; + $file = [ 'help' => __d('cake_console', 'File name to read and write.'), - ); - $name = array( + ]; + $name = [ 'help' => __d('cake_console', 'Classname to use. If its Plugin.class, both name and plugin options will be set.' ) - ); - $snapshot = array( + ]; + $snapshot = [ 'short' => 's', 'help' => __d('cake_console', 'Snapshot number to use/make.') - ); - $models = array( + ]; + $models = [ 'short' => 'm', 'help' => __d('cake_console', 'Specify models as comma separated list.'), - ); - $dry = array( + ]; + $dry = [ 'help' => __d('cake_console', 'Perform a dry run on create and update commands. Queries will be output instead of run.' ), 'boolean' => true - ); - $force = array( + ]; + $force = [ 'short' => 'f', 'help' => __d('cake_console', 'Force "generate" to create a new schema'), 'boolean' => true - ); - $write = array( + ]; + $write = [ 'help' => __d('cake_console', 'Write the dumped SQL to a file.') - ); - $exclude = array( + ]; + $exclude = [ 'help' => __d('cake_console', 'Tables to exclude as comma separated list.') - ); - $yes = array( + ]; + $yes = [ 'short' => 'y', 'help' => __d('cake_console', 'Do not prompt for confirmation. Be careful!'), 'boolean' => true - ); + ]; $parser->description( __d('cake_console', 'The Schema Shell generates a schema object from the database and updates the database from the schema.') - )->addSubcommand('view', array( + )->addSubcommand('view', [ 'help' => __d('cake_console', 'Read and output the contents of a schema file'), - 'parser' => array( + 'parser' => [ 'options' => compact('plugin', 'path', 'file', 'name', 'connection'), 'arguments' => compact('name') - ) - ))->addSubcommand('generate', array( + ] + ])->addSubcommand('generate', [ 'help' => __d('cake_console', 'Reads from --connection and writes to --path. Generate snapshots with -s'), - 'parser' => array( + 'parser' => [ 'options' => compact('plugin', 'path', 'file', 'name', 'connection', 'snapshot', 'force', 'models', 'exclude'), - 'arguments' => array( - 'snapshot' => array('help' => __d('cake_console', 'Generate a snapshot.')) - ) - ) - ))->addSubcommand('dump', array( + 'arguments' => [ + 'snapshot' => ['help' => __d('cake_console', 'Generate a snapshot.')] + ] + ] + ])->addSubcommand('dump', [ 'help' => __d('cake_console', 'Dump database SQL based on a schema file to stdout.'), - 'parser' => array( + 'parser' => [ 'options' => compact('plugin', 'path', 'file', 'name', 'connection', 'write'), 'arguments' => compact('name') - ) - ))->addSubcommand('create', array( + ] + ])->addSubcommand('create', [ 'help' => __d('cake_console', 'Drop and create tables based on the schema file.'), - 'parser' => array( + 'parser' => [ 'options' => compact('plugin', 'path', 'file', 'name', 'connection', 'dry', 'snapshot', 'yes'), - 'args' => array( - 'name' => array( + 'args' => [ + 'name' => [ 'help' => __d('cake_console', 'Name of schema to use.') - ), - 'table' => array( + ], + 'table' => [ 'help' => __d('cake_console', 'Only create the specified table.') - ) - ) - ) - ))->addSubcommand('update', array( + ] + ] + ] + ])->addSubcommand('update', [ 'help' => __d('cake_console', 'Alter the tables based on the schema file.'), - 'parser' => array( + 'parser' => [ 'options' => compact('plugin', 'path', 'file', 'name', 'connection', 'dry', 'snapshot', 'force', 'yes'), - 'args' => array( - 'name' => array( + 'args' => [ + 'name' => [ 'help' => __d('cake_console', 'Name of schema to use.') - ), - 'table' => array( + ], + 'table' => [ 'help' => __d('cake_console', 'Only create the specified table.') - ) - ) - ) - )); + ] + ] + ] + ]); return $parser; } diff --git a/lib/Cake/Console/Command/ServerShell.php b/lib/Cake/Console/Command/ServerShell.php index 014d3978..6e7b6a13 100755 --- a/lib/Cake/Console/Command/ServerShell.php +++ b/lib/Cake/Console/Command/ServerShell.php @@ -22,146 +22,152 @@ * * @package Cake.Console.Command */ -class ServerShell extends AppShell { - -/** - * Default ServerHost - * - * @var string - */ - const DEFAULT_HOST = 'localhost'; - -/** - * Default ListenPort - * - * @var int - */ - const DEFAULT_PORT = 80; - -/** - * server host - * - * @var string - */ - protected $_host = null; - -/** - * listen port - * - * @var string - */ - protected $_port = null; - -/** - * document root - * - * @var string - */ - protected $_documentRoot = null; - -/** - * Override initialize of the Shell - * - * @return void - */ - public function initialize() { - $this->_host = static::DEFAULT_HOST; - $this->_port = static::DEFAULT_PORT; - $this->_documentRoot = WWW_ROOT; - } - -/** - * Starts up the Shell and displays the welcome message. - * Allows for checking and configuring prior to command or main execution - * - * Override this method if you want to remove the welcome information, - * or otherwise modify the pre-command flow. - * - * @return void - * @link https://book.cakephp.org/2.0/en/console-and-shells.html#Shell::startup - */ - public function startup() { - if (!empty($this->params['host'])) { - $this->_host = $this->params['host']; - } - if (!empty($this->params['port'])) { - $this->_port = $this->params['port']; - } - if (!empty($this->params['document_root'])) { - $this->_documentRoot = $this->params['document_root']; - } - - // for Windows - if (substr($this->_documentRoot, -1, 1) === DIRECTORY_SEPARATOR) { - $this->_documentRoot = substr($this->_documentRoot, 0, strlen($this->_documentRoot) - 1); - } - if (preg_match("/^([a-z]:)[\\\]+(.+)$/i", $this->_documentRoot, $m)) { - $this->_documentRoot = $m[1] . '\\' . $m[2]; - } - - parent::startup(); - } - -/** - * Displays a header for the shell - * - * @return void - */ - protected function _welcome() { - $this->out(); - $this->out(__d('cake_console', 'Welcome to CakePHP %s Console', 'v' . Configure::version())); - $this->hr(); - $this->out(__d('cake_console', 'App : %s', APP_DIR)); - $this->out(__d('cake_console', 'Path: %s', APP)); - $this->out(__d('cake_console', 'DocumentRoot: %s', $this->_documentRoot)); - $this->hr(); - } - -/** - * Override main() to handle action - * - * @return void - */ - public function main() { - if (version_compare(PHP_VERSION, '5.4.0') < 0) { - $this->out(__d('cake_console', 'This command is available on %s or above', 'PHP5.4')); - return; - } - - $command = sprintf("php -S %s:%d -t %s %s", - $this->_host, - $this->_port, - escapeshellarg($this->_documentRoot), - escapeshellarg($this->_documentRoot . '/index.php') - ); - - $port = ($this->_port == static::DEFAULT_PORT) ? '' : ':' . $this->_port; - $this->out(__d('cake_console', 'built-in server is running in http://%s%s/', $this->_host, $port)); - system($command); - } - -/** - * Gets the option parser instance and configures it. - * - * @return ConsoleOptionParser - */ - public function getOptionParser() { - $parser = parent::getOptionParser(); - - $parser->description(array( - __d('cake_console', 'PHP Built-in Server for CakePHP'), - __d('cake_console', '[WARN] Don\'t use this at the production environment') - ))->addOption('host', array( - 'short' => 'H', - 'help' => __d('cake_console', 'ServerHost') - ))->addOption('port', array( - 'short' => 'p', - 'help' => __d('cake_console', 'ListenPort') - ))->addOption('document_root', array( - 'short' => 'd', - 'help' => __d('cake_console', 'DocumentRoot') - )); - - return $parser; - } +class ServerShell extends AppShell +{ + + /** + * Default ServerHost + * + * @var string + */ + const DEFAULT_HOST = 'localhost'; + + /** + * Default ListenPort + * + * @var int + */ + const DEFAULT_PORT = 80; + + /** + * server host + * + * @var string + */ + protected $_host = null; + + /** + * listen port + * + * @var string + */ + protected $_port = null; + + /** + * document root + * + * @var string + */ + protected $_documentRoot = null; + + /** + * Override initialize of the Shell + * + * @return void + */ + public function initialize() + { + $this->_host = static::DEFAULT_HOST; + $this->_port = static::DEFAULT_PORT; + $this->_documentRoot = WWW_ROOT; + } + + /** + * Starts up the Shell and displays the welcome message. + * Allows for checking and configuring prior to command or main execution + * + * Override this method if you want to remove the welcome information, + * or otherwise modify the pre-command flow. + * + * @return void + * @link https://book.cakephp.org/2.0/en/console-and-shells.html#Shell::startup + */ + public function startup() + { + if (!empty($this->params['host'])) { + $this->_host = $this->params['host']; + } + if (!empty($this->params['port'])) { + $this->_port = $this->params['port']; + } + if (!empty($this->params['document_root'])) { + $this->_documentRoot = $this->params['document_root']; + } + + // for Windows + if (substr($this->_documentRoot, -1, 1) === DIRECTORY_SEPARATOR) { + $this->_documentRoot = substr($this->_documentRoot, 0, strlen($this->_documentRoot) - 1); + } + if (preg_match("/^([a-z]:)[\\\]+(.+)$/i", $this->_documentRoot, $m)) { + $this->_documentRoot = $m[1] . '\\' . $m[2]; + } + + parent::startup(); + } + + /** + * Override main() to handle action + * + * @return void + */ + public function main() + { + if (version_compare(PHP_VERSION, '5.4.0') < 0) { + $this->out(__d('cake_console', 'This command is available on %s or above', 'PHP5.4')); + return; + } + + $command = sprintf("php -S %s:%d -t %s %s", + $this->_host, + $this->_port, + escapeshellarg($this->_documentRoot), + escapeshellarg($this->_documentRoot . '/index.php') + ); + + $port = ($this->_port == static::DEFAULT_PORT) ? '' : ':' . $this->_port; + $this->out(__d('cake_console', 'built-in server is running in http://%s%s/', $this->_host, $port)); + system($command); + } + + /** + * Gets the option parser instance and configures it. + * + * @return ConsoleOptionParser + */ + public function getOptionParser() + { + $parser = parent::getOptionParser(); + + $parser->description([ + __d('cake_console', 'PHP Built-in Server for CakePHP'), + __d('cake_console', '[WARN] Don\'t use this at the production environment') + ])->addOption('host', [ + 'short' => 'H', + 'help' => __d('cake_console', 'ServerHost') + ])->addOption('port', [ + 'short' => 'p', + 'help' => __d('cake_console', 'ListenPort') + ])->addOption('document_root', [ + 'short' => 'd', + 'help' => __d('cake_console', 'DocumentRoot') + ]); + + return $parser; + } + + /** + * Displays a header for the shell + * + * @return void + */ + protected function _welcome() + { + $this->out(); + $this->out(__d('cake_console', 'Welcome to CakePHP %s Console', 'v' . Configure::version())); + $this->hr(); + $this->out(__d('cake_console', 'App : %s', APP_DIR)); + $this->out(__d('cake_console', 'Path: %s', APP)); + $this->out(__d('cake_console', 'DocumentRoot: %s', $this->_documentRoot)); + $this->hr(); + } } diff --git a/lib/Cake/Console/Command/Task/BakeTask.php b/lib/Cake/Console/Command/Task/BakeTask.php index c886c95e..53632083 100755 --- a/lib/Cake/Console/Command/Task/BakeTask.php +++ b/lib/Cake/Console/Command/Task/BakeTask.php @@ -22,71 +22,75 @@ * * @package Cake.Console.Command.Task */ -class BakeTask extends AppShell { +class BakeTask extends AppShell +{ -/** - * Name of plugin - * - * @var string - */ - public $plugin = null; + /** + * Name of plugin + * + * @var string + */ + public $plugin = null; -/** - * The db connection being used for baking - * - * @var string - */ - public $connection = null; + /** + * The db connection being used for baking + * + * @var string + */ + public $connection = null; -/** - * Flag for interactive mode - * - * @var bool - */ - public $interactive = false; + /** + * Flag for interactive mode + * + * @var bool + */ + public $interactive = false; -/** - * Disable caching and enable debug for baking. - * This forces the most current database schema to be used. - * - * @return void - */ - public function startup() { - Configure::write('debug', 2); - Configure::write('Cache.disable', 1); - parent::startup(); - } + /** + * Disable caching and enable debug for baking. + * This forces the most current database schema to be used. + * + * @return void + */ + public function startup() + { + Configure::write('debug', 2); + Configure::write('Cache.disable', 1); + parent::startup(); + } -/** - * Gets the path for output. Checks the plugin property - * and returns the correct path. - * - * @return string Path to output. - */ - public function getPath() { - $path = $this->path; - if (isset($this->plugin)) { - $path = $this->_pluginPath($this->plugin) . $this->name . DS; - } - return $path; - } + /** + * Gets the path for output. Checks the plugin property + * and returns the correct path. + * + * @return string Path to output. + */ + public function getPath() + { + $path = $this->path; + if (isset($this->plugin)) { + $path = $this->_pluginPath($this->plugin) . $this->name . DS; + } + return $path; + } -/** - * Base execute method parses some parameters and sets some properties on the bake tasks. - * call when overriding execute() - * - * @return void - */ - public function execute() { - foreach ($this->args as $i => $arg) { - if (strpos($arg, '.')) { - list($this->params['plugin'], $this->args[$i]) = pluginSplit($arg); - break; - } - } - if (isset($this->params['plugin'])) { - $this->plugin = $this->params['plugin']; - } - } + /** + * Base execute method parses some parameters and sets some properties on the bake tasks. + * call when overriding execute() + * + * @return void + */ + public function execute() + { + foreach ($this->args as $i => $arg) { + if (strpos($arg, '.')) { + list($this->params['plugin'], $this->args[$i]) = pluginSplit($arg); + break; + } + } + if (isset($this->params['plugin'])) { + $this->plugin = $this->params['plugin']; + } + } } diff --git a/lib/Cake/Console/Command/Task/CommandTask.php b/lib/Cake/Console/Command/Task/CommandTask.php index 99c888f8..6e598a95 100755 --- a/lib/Cake/Console/Command/Task/CommandTask.php +++ b/lib/Cake/Console/Command/Task/CommandTask.php @@ -20,164 +20,171 @@ * * @package Cake.Console.Command.Task */ -class CommandTask extends AppShell { - -/** - * Gets the shell command listing. - * - * @return array - */ - public function getShellList() { - $skipFiles = array('AppShell'); - - $plugins = CakePlugin::loaded(); - $shellList = array_fill_keys($plugins, null) + array('CORE' => null, 'app' => null); - - $corePath = App::core('Console/Command'); - $shells = App::objects('file', $corePath[0]); - $shells = array_diff($shells, $skipFiles); - $this->_appendShells('CORE', $shells, $shellList); - - $appShells = App::objects('Console/Command', null, false); - $appShells = array_diff($appShells, $shells, $skipFiles); - $this->_appendShells('app', $appShells, $shellList); - - foreach ($plugins as $plugin) { - $pluginShells = App::objects($plugin . '.Console/Command'); - $this->_appendShells($plugin, $pluginShells, $shellList); - } - - return array_filter($shellList); - } - -/** - * Scan the provided paths for shells, and append them into $shellList - * - * @param string $type The type of object. - * @param array $shells The shell name. - * @param array &$shellList List of shells. - * @return void - */ - protected function _appendShells($type, $shells, &$shellList) { - foreach ($shells as $shell) { - $shellList[$type][] = Inflector::underscore(str_replace('Shell', '', $shell)); - } - } - -/** - * Return a list of all commands - * - * @return array - */ - public function commands() { - $shellList = $this->getShellList(); - - $options = array(); - foreach ($shellList as $type => $commands) { - $prefix = ''; - if (!in_array(strtolower($type), array('app', 'core'))) { - $prefix = $type . '.'; - } - - foreach ($commands as $shell) { - $options[] = $prefix . $shell; - } - } - - return $options; - } - -/** - * Return a list of subcommands for a given command - * - * @param string $commandName The command you want subcommands from. - * @return array - */ - public function subCommands($commandName) { - $Shell = $this->getShell($commandName); - - if (!$Shell) { - return array(); - } - - $taskMap = TaskCollection::normalizeObjectArray((array)$Shell->tasks); - $return = array_keys($taskMap); - $return = array_map('Inflector::underscore', $return); - - $ShellReflection = new ReflectionClass('AppShell'); - $shellMethods = $ShellReflection->getMethods(ReflectionMethod::IS_PUBLIC); - $shellMethodNames = array('main', 'help'); - foreach ($shellMethods as $method) { - $shellMethodNames[] = $method->getName(); - } - - $Reflection = new ReflectionClass($Shell); - $methods = $Reflection->getMethods(ReflectionMethod::IS_PUBLIC); - $methodNames = array(); - foreach ($methods as $method) { - $methodNames[] = $method->getName(); - } - - $return += array_diff($methodNames, $shellMethodNames); - sort($return); - - return $return; - } - -/** - * Get Shell instance for the given command - * - * @param mixed $commandName The command you want. - * @return mixed - */ - public function getShell($commandName) { - list($pluginDot, $name) = pluginSplit($commandName, true); - - if (in_array(strtolower($pluginDot), array('app.', 'core.'))) { - $commandName = $name; - $pluginDot = ''; - } - - if (!in_array($commandName, $this->commands())) { - return false; - } - - $name = Inflector::camelize($name); - $pluginDot = Inflector::camelize($pluginDot); - $class = $name . 'Shell'; - App::uses($class, $pluginDot . 'Console/Command'); - - $Shell = new $class(); - $Shell->plugin = trim($pluginDot, '.'); - $Shell->initialize(); - - return $Shell; - } - -/** - * Get Shell instance for the given command - * - * @param mixed $commandName The command to get options for. - * @return array - */ - public function options($commandName) { - $Shell = $this->getShell($commandName); - if (!$Shell) { - $parser = new ConsoleOptionParser(); - } else { - $parser = $Shell->getOptionParser(); - } - - $options = array(); - $array = $parser->options(); - foreach ($array as $name => $obj) { - $options[] = "--$name"; - $short = $obj->short(); - if ($short) { - $options[] = "-$short"; - } - } - return $options; - } +class CommandTask extends AppShell +{ + + /** + * Return a list of subcommands for a given command + * + * @param string $commandName The command you want subcommands from. + * @return array + */ + public function subCommands($commandName) + { + $Shell = $this->getShell($commandName); + + if (!$Shell) { + return []; + } + + $taskMap = TaskCollection::normalizeObjectArray((array)$Shell->tasks); + $return = array_keys($taskMap); + $return = array_map('Inflector::underscore', $return); + + $ShellReflection = new ReflectionClass('AppShell'); + $shellMethods = $ShellReflection->getMethods(ReflectionMethod::IS_PUBLIC); + $shellMethodNames = ['main', 'help']; + foreach ($shellMethods as $method) { + $shellMethodNames[] = $method->getName(); + } + + $Reflection = new ReflectionClass($Shell); + $methods = $Reflection->getMethods(ReflectionMethod::IS_PUBLIC); + $methodNames = []; + foreach ($methods as $method) { + $methodNames[] = $method->getName(); + } + + $return += array_diff($methodNames, $shellMethodNames); + sort($return); + + return $return; + } + + /** + * Get Shell instance for the given command + * + * @param mixed $commandName The command you want. + * @return mixed + */ + public function getShell($commandName) + { + list($pluginDot, $name) = pluginSplit($commandName, true); + + if (in_array(strtolower($pluginDot), ['app.', 'core.'])) { + $commandName = $name; + $pluginDot = ''; + } + + if (!in_array($commandName, $this->commands())) { + return false; + } + + $name = Inflector::camelize($name); + $pluginDot = Inflector::camelize($pluginDot); + $class = $name . 'Shell'; + App::uses($class, $pluginDot . 'Console/Command'); + + $Shell = new $class(); + $Shell->plugin = trim($pluginDot, '.'); + $Shell->initialize(); + + return $Shell; + } + + /** + * Return a list of all commands + * + * @return array + */ + public function commands() + { + $shellList = $this->getShellList(); + + $options = []; + foreach ($shellList as $type => $commands) { + $prefix = ''; + if (!in_array(strtolower($type), ['app', 'core'])) { + $prefix = $type . '.'; + } + + foreach ($commands as $shell) { + $options[] = $prefix . $shell; + } + } + + return $options; + } + + /** + * Gets the shell command listing. + * + * @return array + */ + public function getShellList() + { + $skipFiles = ['AppShell']; + + $plugins = CakePlugin::loaded(); + $shellList = array_fill_keys($plugins, null) + ['CORE' => null, 'app' => null]; + + $corePath = App::core('Console/Command'); + $shells = App::objects('file', $corePath[0]); + $shells = array_diff($shells, $skipFiles); + $this->_appendShells('CORE', $shells, $shellList); + + $appShells = App::objects('Console/Command', null, false); + $appShells = array_diff($appShells, $shells, $skipFiles); + $this->_appendShells('app', $appShells, $shellList); + + foreach ($plugins as $plugin) { + $pluginShells = App::objects($plugin . '.Console/Command'); + $this->_appendShells($plugin, $pluginShells, $shellList); + } + + return array_filter($shellList); + } + + /** + * Scan the provided paths for shells, and append them into $shellList + * + * @param string $type The type of object. + * @param array $shells The shell name. + * @param array &$shellList List of shells. + * @return void + */ + protected function _appendShells($type, $shells, &$shellList) + { + foreach ($shells as $shell) { + $shellList[$type][] = Inflector::underscore(str_replace('Shell', '', $shell)); + } + } + + /** + * Get Shell instance for the given command + * + * @param mixed $commandName The command to get options for. + * @return array + */ + public function options($commandName) + { + $Shell = $this->getShell($commandName); + if (!$Shell) { + $parser = new ConsoleOptionParser(); + } else { + $parser = $Shell->getOptionParser(); + } + + $options = []; + $array = $parser->options(); + foreach ($array as $name => $obj) { + $options[] = "--$name"; + $short = $obj->short(); + if ($short) { + $options[] = "-$short"; + } + } + return $options; + } } diff --git a/lib/Cake/Console/Command/Task/ControllerTask.php b/lib/Cake/Console/Command/Task/ControllerTask.php index 433329d6..239ef5b4 100755 --- a/lib/Cake/Console/Command/Task/ControllerTask.php +++ b/lib/Cake/Console/Command/Task/ControllerTask.php @@ -24,491 +24,507 @@ * * @package Cake.Console.Command.Task */ -class ControllerTask extends BakeTask { - -/** - * Tasks to be loaded by this Task - * - * @var array - */ - public $tasks = array('Model', 'Test', 'Template', 'DbConfig', 'Project'); - -/** - * path to Controller directory - * - * @var array - */ - public $path = null; - -/** - * Override initialize - * - * @return void - */ - public function initialize() { - $this->path = current(App::path('Controller')); - } - -/** - * Execution method always used for tasks - * - * @return void - */ - public function execute() { - parent::execute(); - if (empty($this->args)) { - return $this->_interactive(); - } - - if (isset($this->args[0])) { - if (!isset($this->connection)) { - $this->connection = 'default'; - } - if (strtolower($this->args[0]) === 'all') { - return $this->all(); - } - - $controller = $this->_controllerName($this->args[0]); - $actions = ''; - - if (!empty($this->params['public'])) { - $this->out(__d('cake_console', 'Baking basic crud methods for ') . $controller); - $actions .= $this->bakeActions($controller); - } - if (!empty($this->params['admin'])) { - $admin = $this->Project->getPrefix(); - if ($admin) { - $this->out(__d('cake_console', 'Adding %s methods', $admin)); - $actions .= "\n" . $this->bakeActions($controller, $admin); - } - } - if (empty($actions)) { - $actions = 'scaffold'; - } - - if ($this->bake($controller, $actions)) { - if ($this->_checkUnitTest()) { - $this->bakeTest($controller); - } - } - } - } - -/** - * Bake All the controllers at once. Will only bake controllers for models that exist. - * - * @return void - */ - public function all() { - $this->interactive = false; - $this->listAll($this->connection, false); - ClassRegistry::config('Model', array('ds' => $this->connection)); - $unitTestExists = $this->_checkUnitTest(); - - $admin = false; - if (!empty($this->params['admin'])) { - $admin = $this->Project->getPrefix(); - } - - $controllersCreated = 0; - foreach ($this->__tables as $table) { - $model = $this->_modelName($table); - $controller = $this->_controllerName($model); - App::uses($model, 'Model'); - if (class_exists($model)) { - $actions = $this->bakeActions($controller); - if ($admin) { - $this->out(__d('cake_console', 'Adding %s methods', $admin)); - $actions .= "\n" . $this->bakeActions($controller, $admin); - } - if ($this->bake($controller, $actions) && $unitTestExists) { - $this->bakeTest($controller); - } - $controllersCreated++; - } - } - - if (!$controllersCreated) { - $this->out(__d('cake_console', 'No Controllers were baked, Models need to exist before Controllers can be baked.')); - } - } - -/** - * Interactive - * - * @return void - */ - protected function _interactive() { - $this->interactive = true; - $this->hr(); - $this->out(__d('cake_console', "Bake Controller\nPath: %s", $this->getPath())); - $this->hr(); - - if (empty($this->connection)) { - $this->connection = $this->DbConfig->getConfig(); - } - - $controllerName = $this->getName(); - $this->hr(); - $this->out(__d('cake_console', 'Baking %sController', $controllerName)); - $this->hr(); - - $helpers = $components = array(); - $actions = ''; - $wannaUseSession = 'y'; - $wannaBakeAdminCrud = 'n'; - $useDynamicScaffold = 'n'; - $wannaBakeCrud = 'y'; - - $question[] = __d('cake_console', "Would you like to build your controller interactively?"); - if (file_exists($this->path . $controllerName . 'Controller.php')) { - $question[] = __d('cake_console', "Warning: Choosing no will overwrite the %sController.", $controllerName); - } - $doItInteractive = $this->in(implode("\n", $question), array('y', 'n'), 'y'); - - if (strtolower($doItInteractive) === 'y') { - $this->interactive = true; - $useDynamicScaffold = $this->in( - __d('cake_console', "Would you like to use dynamic scaffolding?"), array('y', 'n'), 'n' - ); - - if (strtolower($useDynamicScaffold) === 'y') { - $wannaBakeCrud = 'n'; - $actions = 'scaffold'; - } else { - list($wannaBakeCrud, $wannaBakeAdminCrud) = $this->_askAboutMethods(); - - $helpers = $this->doHelpers(); - $components = $this->doComponents(); - - $wannaUseSession = $this->in( - __d('cake_console', "Would you like to use the FlashComponent to display flash messages?"), array('y', 'n'), 'y' - ); - - if (strtolower($wannaUseSession) === 'y') { - array_push($components, 'Session', 'Flash'); - } - array_unique($components); - } - } else { - list($wannaBakeCrud, $wannaBakeAdminCrud) = $this->_askAboutMethods(); - } - - if (strtolower($wannaBakeCrud) === 'y') { - $actions = $this->bakeActions($controllerName, null, strtolower($wannaUseSession) === 'y'); - } - if (strtolower($wannaBakeAdminCrud) === 'y') { - $admin = $this->Project->getPrefix(); - $actions .= $this->bakeActions($controllerName, $admin, strtolower($wannaUseSession) === 'y'); - } - - $baked = false; - if ($this->interactive === true) { - $this->confirmController($controllerName, $useDynamicScaffold, $helpers, $components); - $looksGood = $this->in(__d('cake_console', 'Look okay?'), array('y', 'n'), 'y'); - - if (strtolower($looksGood) === 'y') { - $baked = $this->bake($controllerName, $actions, $helpers, $components); - if ($baked && $this->_checkUnitTest()) { - $this->bakeTest($controllerName); - } - } - } else { - $baked = $this->bake($controllerName, $actions, $helpers, $components); - if ($baked && $this->_checkUnitTest()) { - $this->bakeTest($controllerName); - } - } - return $baked; - } - -/** - * Confirm a to be baked controller with the user - * - * @param string $controllerName The name of the controller. - * @param string $useDynamicScaffold Whether or not to use dynamic scaffolds. - * @param array $helpers The list of helpers to include. - * @param array $components The list of components to include. - * @return void - */ - public function confirmController($controllerName, $useDynamicScaffold, $helpers, $components) { - $this->out(); - $this->hr(); - $this->out(__d('cake_console', 'The following controller will be created:')); - $this->hr(); - $this->out(__d('cake_console', "Controller Name:\n\t%s", $controllerName)); - - if (strtolower($useDynamicScaffold) === 'y') { - $this->out("public \$scaffold;"); - } - - $properties = array( - 'helpers' => __d('cake_console', 'Helpers:'), - 'components' => __d('cake_console', 'Components:'), - ); - - foreach ($properties as $var => $title) { - if (count(${$var})) { - $output = ''; - $length = count(${$var}); - foreach (${$var} as $i => $propElement) { - if ($i != $length - 1) { - $output .= ucfirst($propElement) . ', '; - } else { - $output .= ucfirst($propElement); - } - } - $this->out($title . "\n\t" . $output); - } - } - $this->hr(); - } - -/** - * Interact with the user and ask about which methods (admin or regular they want to bake) - * - * @return array Array containing (bakeRegular, bakeAdmin) answers - */ - protected function _askAboutMethods() { - $wannaBakeCrud = $this->in( - __d('cake_console', "Would you like to create some basic class methods \n(index(), add(), view(), edit())?"), - array('y', 'n'), 'n' - ); - $wannaBakeAdminCrud = $this->in( - __d('cake_console', "Would you like to create the basic class methods for admin routing?"), - array('y', 'n'), 'n' - ); - return array($wannaBakeCrud, $wannaBakeAdminCrud); - } - -/** - * Bake scaffold actions - * - * @param string $controllerName Controller name - * @param string $admin Admin route to use - * @param bool $wannaUseSession Set to true to use sessions, false otherwise - * @return string Baked actions - */ - public function bakeActions($controllerName, $admin = null, $wannaUseSession = true) { - $currentModelName = $modelImport = $this->_modelName($controllerName); - $plugin = $this->plugin; - if ($plugin) { - $plugin .= '.'; - } - App::uses($modelImport, $plugin . 'Model'); - if (!class_exists($modelImport)) { - $this->err(__d('cake_console', 'You must have a model for this class to build basic methods. Please try again.')); - return $this->_stop(); - } - - $modelObj = ClassRegistry::init($currentModelName); - $controllerPath = $this->_controllerPath($controllerName); - $pluralName = $this->_pluralName($currentModelName); - $singularName = Inflector::variable($currentModelName); - $singularHumanName = $this->_singularHumanName($controllerName); - $pluralHumanName = $this->_pluralName($controllerName); - $displayField = $modelObj->displayField; - $primaryKey = $modelObj->primaryKey; - - $this->Template->set(compact( - 'plugin', 'admin', 'controllerPath', 'pluralName', 'singularName', - 'singularHumanName', 'pluralHumanName', 'modelObj', 'wannaUseSession', 'currentModelName', - 'displayField', 'primaryKey' - )); - $actions = $this->Template->generate('actions', 'controller_actions'); - return $actions; - } - -/** - * Assembles and writes a Controller file - * - * @param string $controllerName Controller name already pluralized and correctly cased. - * @param string $actions Actions to add, or set the whole controller to use $scaffold (set $actions to 'scaffold') - * @param array $helpers Helpers to use in controller - * @param array $components Components to use in controller - * @return string Baked controller - */ - public function bake($controllerName, $actions = '', $helpers = null, $components = null) { - $this->out("\n" . __d('cake_console', 'Baking controller class for %s...', $controllerName), 1, Shell::QUIET); - - if ($helpers === null) { - $helpers = array(); - } - if ($components === null) { - $components = array(); - } - $isScaffold = ($actions === 'scaffold') ? true : false; - - $this->Template->set(array( - 'plugin' => $this->plugin, - 'pluginPath' => empty($this->plugin) ? '' : $this->plugin . '.' - )); - - if (!in_array('Paginator', (array)$components)) { - $components[] = 'Paginator'; - } - - $this->Template->set(compact('controllerName', 'actions', 'helpers', 'components', 'isScaffold')); - $contents = $this->Template->generate('classes', 'controller'); - - $path = $this->getPath(); - $filename = $path . $controllerName . 'Controller.php'; - if ($this->createFile($filename, $contents)) { - return $contents; - } - return false; - } - -/** - * Assembles and writes a unit test file - * - * @param string $className Controller class name - * @return string Baked test - */ - public function bakeTest($className) { - $this->Test->plugin = $this->plugin; - $this->Test->connection = $this->connection; - $this->Test->interactive = $this->interactive; - return $this->Test->bake('Controller', $className); - } - -/** - * Interact with the user and get a list of additional helpers - * - * @return array Helpers that the user wants to use. - */ - public function doHelpers() { - return $this->_doPropertyChoices( - __d('cake_console', "Would you like this controller to use other helpers\nbesides HtmlHelper and FormHelper?"), - __d('cake_console', "Please provide a comma separated list of the other\nhelper names you'd like to use.\nExample: 'Text, Js, Time'") - ); - } - -/** - * Interact with the user and get a list of additional components - * - * @return array Components the user wants to use. - */ - public function doComponents() { - $components = array('Paginator'); - return array_merge($components, $this->_doPropertyChoices( - __d('cake_console', "Would you like this controller to use other components\nbesides PaginatorComponent?"), - __d('cake_console', "Please provide a comma separated list of the component names you'd like to use.\nExample: 'Acl, Security, RequestHandler'") - )); - } - -/** - * Common code for property choice handling. - * - * @param string $prompt A yes/no question to precede the list - * @param string $example A question for a comma separated list, with examples. - * @return array Array of values for property. - */ - protected function _doPropertyChoices($prompt, $example) { - $proceed = $this->in($prompt, array('y', 'n'), 'n'); - $property = array(); - if (strtolower($proceed) === 'y') { - $propertyList = $this->in($example); - $propertyListTrimmed = str_replace(' ', '', $propertyList); - $property = explode(',', $propertyListTrimmed); - } - return array_filter($property); - } - -/** - * Outputs and gets the list of possible controllers from database - * - * @param string $useDbConfig Database configuration name - * @return array Set of controllers - */ - public function listAll($useDbConfig = null) { - if ($useDbConfig === null) { - $useDbConfig = $this->connection; - } - $this->__tables = $this->Model->getAllTables($useDbConfig); - - if ($this->interactive) { - $this->out(__d('cake_console', 'Possible Controllers based on your current database:')); - $this->hr(); - $this->_controllerNames = array(); - $count = count($this->__tables); - for ($i = 0; $i < $count; $i++) { - $this->_controllerNames[] = $this->_controllerName($this->_modelName($this->__tables[$i])); - $this->out(sprintf("%2d. %s", $i + 1, $this->_controllerNames[$i])); - } - return $this->_controllerNames; - } - return $this->__tables; - } - -/** - * Forces the user to specify the controller he wants to bake, and returns the selected controller name. - * - * @param string $useDbConfig Connection name to get a controller name for. - * @return string Controller name - */ - public function getName($useDbConfig = null) { - $controllers = $this->listAll($useDbConfig); - $enteredController = ''; - - while (!$enteredController) { - $enteredController = $this->in(__d('cake_console', "Enter a number from the list above,\ntype in the name of another controller, or 'q' to exit"), null, 'q'); - if ($enteredController === 'q') { - $this->out(__d('cake_console', 'Exit')); - return $this->_stop(); - } - - if (!$enteredController || (int)$enteredController > count($controllers)) { - $this->err(__d('cake_console', "The Controller name you supplied was empty,\nor the number you selected was not an option. Please try again.")); - $enteredController = ''; - } - } - - if ((int)$enteredController > 0 && (int)$enteredController <= count($controllers)) { - $controllerName = $controllers[(int)$enteredController - 1]; - } else { - $controllerName = Inflector::camelize($enteredController); - } - return $controllerName; - } - -/** - * Gets the option parser instance and configures it. - * - * @return ConsoleOptionParser - */ - public function getOptionParser() { - $parser = parent::getOptionParser(); - - $parser->description( - __d('cake_console', 'Bake a controller for a model. Using options you can bake public, admin or both.' - ))->addArgument('name', array( - 'help' => __d('cake_console', 'Name of the controller to bake. Can use Plugin.name to bake controllers into plugins.') - ))->addOption('public', array( - 'help' => __d('cake_console', 'Bake a controller with basic crud actions (index, view, add, edit, delete).'), - 'boolean' => true - ))->addOption('admin', array( - 'help' => __d('cake_console', 'Bake a controller with crud actions for one of the Routing.prefixes.'), - 'boolean' => true - ))->addOption('plugin', array( - 'short' => 'p', - 'help' => __d('cake_console', 'Plugin to bake the controller into.') - ))->addOption('connection', array( - 'short' => 'c', - 'help' => __d('cake_console', 'The connection the controller\'s model is on.') - ))->addOption('theme', array( - 'short' => 't', - 'help' => __d('cake_console', 'Theme to use when baking code.') - ))->addOption('force', array( - 'short' => 'f', - 'help' => __d('cake_console', 'Force overwriting existing files without prompting.') - ))->addSubcommand('all', array( - 'help' => __d('cake_console', 'Bake all controllers with CRUD methods.') - ))->epilog( - __d('cake_console', 'Omitting all arguments and options will enter into an interactive mode.') - ); - - return $parser; - } +class ControllerTask extends BakeTask +{ + + /** + * Tasks to be loaded by this Task + * + * @var array + */ + public $tasks = ['Model', 'Test', 'Template', 'DbConfig', 'Project']; + + /** + * path to Controller directory + * + * @var array + */ + public $path = null; + + /** + * Override initialize + * + * @return void + */ + public function initialize() + { + $this->path = current(App::path('Controller')); + } + + /** + * Execution method always used for tasks + * + * @return void + */ + public function execute() + { + parent::execute(); + if (empty($this->args)) { + return $this->_interactive(); + } + + if (isset($this->args[0])) { + if (!isset($this->connection)) { + $this->connection = 'default'; + } + if (strtolower($this->args[0]) === 'all') { + return $this->all(); + } + + $controller = $this->_controllerName($this->args[0]); + $actions = ''; + + if (!empty($this->params['public'])) { + $this->out(__d('cake_console', 'Baking basic crud methods for ') . $controller); + $actions .= $this->bakeActions($controller); + } + if (!empty($this->params['admin'])) { + $admin = $this->Project->getPrefix(); + if ($admin) { + $this->out(__d('cake_console', 'Adding %s methods', $admin)); + $actions .= "\n" . $this->bakeActions($controller, $admin); + } + } + if (empty($actions)) { + $actions = 'scaffold'; + } + + if ($this->bake($controller, $actions)) { + if ($this->_checkUnitTest()) { + $this->bakeTest($controller); + } + } + } + } + + /** + * Interactive + * + * @return void + */ + protected function _interactive() + { + $this->interactive = true; + $this->hr(); + $this->out(__d('cake_console', "Bake Controller\nPath: %s", $this->getPath())); + $this->hr(); + + if (empty($this->connection)) { + $this->connection = $this->DbConfig->getConfig(); + } + + $controllerName = $this->getName(); + $this->hr(); + $this->out(__d('cake_console', 'Baking %sController', $controllerName)); + $this->hr(); + + $helpers = $components = []; + $actions = ''; + $wannaUseSession = 'y'; + $wannaBakeAdminCrud = 'n'; + $useDynamicScaffold = 'n'; + $wannaBakeCrud = 'y'; + + $question[] = __d('cake_console', "Would you like to build your controller interactively?"); + if (file_exists($this->path . $controllerName . 'Controller.php')) { + $question[] = __d('cake_console', "Warning: Choosing no will overwrite the %sController.", $controllerName); + } + $doItInteractive = $this->in(implode("\n", $question), ['y', 'n'], 'y'); + + if (strtolower($doItInteractive) === 'y') { + $this->interactive = true; + $useDynamicScaffold = $this->in( + __d('cake_console', "Would you like to use dynamic scaffolding?"), ['y', 'n'], 'n' + ); + + if (strtolower($useDynamicScaffold) === 'y') { + $wannaBakeCrud = 'n'; + $actions = 'scaffold'; + } else { + list($wannaBakeCrud, $wannaBakeAdminCrud) = $this->_askAboutMethods(); + + $helpers = $this->doHelpers(); + $components = $this->doComponents(); + + $wannaUseSession = $this->in( + __d('cake_console', "Would you like to use the FlashComponent to display flash messages?"), ['y', 'n'], 'y' + ); + + if (strtolower($wannaUseSession) === 'y') { + array_push($components, 'Session', 'Flash'); + } + array_unique($components); + } + } else { + list($wannaBakeCrud, $wannaBakeAdminCrud) = $this->_askAboutMethods(); + } + + if (strtolower($wannaBakeCrud) === 'y') { + $actions = $this->bakeActions($controllerName, null, strtolower($wannaUseSession) === 'y'); + } + if (strtolower($wannaBakeAdminCrud) === 'y') { + $admin = $this->Project->getPrefix(); + $actions .= $this->bakeActions($controllerName, $admin, strtolower($wannaUseSession) === 'y'); + } + + $baked = false; + if ($this->interactive === true) { + $this->confirmController($controllerName, $useDynamicScaffold, $helpers, $components); + $looksGood = $this->in(__d('cake_console', 'Look okay?'), ['y', 'n'], 'y'); + + if (strtolower($looksGood) === 'y') { + $baked = $this->bake($controllerName, $actions, $helpers, $components); + if ($baked && $this->_checkUnitTest()) { + $this->bakeTest($controllerName); + } + } + } else { + $baked = $this->bake($controllerName, $actions, $helpers, $components); + if ($baked && $this->_checkUnitTest()) { + $this->bakeTest($controllerName); + } + } + return $baked; + } + + /** + * Forces the user to specify the controller he wants to bake, and returns the selected controller name. + * + * @param string $useDbConfig Connection name to get a controller name for. + * @return string Controller name + */ + public function getName($useDbConfig = null) + { + $controllers = $this->listAll($useDbConfig); + $enteredController = ''; + + while (!$enteredController) { + $enteredController = $this->in(__d('cake_console', "Enter a number from the list above,\ntype in the name of another controller, or 'q' to exit"), null, 'q'); + if ($enteredController === 'q') { + $this->out(__d('cake_console', 'Exit')); + return $this->_stop(); + } + + if (!$enteredController || (int)$enteredController > count($controllers)) { + $this->err(__d('cake_console', "The Controller name you supplied was empty,\nor the number you selected was not an option. Please try again.")); + $enteredController = ''; + } + } + + if ((int)$enteredController > 0 && (int)$enteredController <= count($controllers)) { + $controllerName = $controllers[(int)$enteredController - 1]; + } else { + $controllerName = Inflector::camelize($enteredController); + } + return $controllerName; + } + + /** + * Outputs and gets the list of possible controllers from database + * + * @param string $useDbConfig Database configuration name + * @return array Set of controllers + */ + public function listAll($useDbConfig = null) + { + if ($useDbConfig === null) { + $useDbConfig = $this->connection; + } + $this->__tables = $this->Model->getAllTables($useDbConfig); + + if ($this->interactive) { + $this->out(__d('cake_console', 'Possible Controllers based on your current database:')); + $this->hr(); + $this->_controllerNames = []; + $count = count($this->__tables); + for ($i = 0; $i < $count; $i++) { + $this->_controllerNames[] = $this->_controllerName($this->_modelName($this->__tables[$i])); + $this->out(sprintf("%2d. %s", $i + 1, $this->_controllerNames[$i])); + } + return $this->_controllerNames; + } + return $this->__tables; + } + + /** + * Interact with the user and ask about which methods (admin or regular they want to bake) + * + * @return array Array containing (bakeRegular, bakeAdmin) answers + */ + protected function _askAboutMethods() + { + $wannaBakeCrud = $this->in( + __d('cake_console', "Would you like to create some basic class methods \n(index(), add(), view(), edit())?"), + ['y', 'n'], 'n' + ); + $wannaBakeAdminCrud = $this->in( + __d('cake_console', "Would you like to create the basic class methods for admin routing?"), + ['y', 'n'], 'n' + ); + return [$wannaBakeCrud, $wannaBakeAdminCrud]; + } + + /** + * Interact with the user and get a list of additional helpers + * + * @return array Helpers that the user wants to use. + */ + public function doHelpers() + { + return $this->_doPropertyChoices( + __d('cake_console', "Would you like this controller to use other helpers\nbesides HtmlHelper and FormHelper?"), + __d('cake_console', "Please provide a comma separated list of the other\nhelper names you'd like to use.\nExample: 'Text, Js, Time'") + ); + } + + /** + * Common code for property choice handling. + * + * @param string $prompt A yes/no question to precede the list + * @param string $example A question for a comma separated list, with examples. + * @return array Array of values for property. + */ + protected function _doPropertyChoices($prompt, $example) + { + $proceed = $this->in($prompt, ['y', 'n'], 'n'); + $property = []; + if (strtolower($proceed) === 'y') { + $propertyList = $this->in($example); + $propertyListTrimmed = str_replace(' ', '', $propertyList); + $property = explode(',', $propertyListTrimmed); + } + return array_filter($property); + } + + /** + * Interact with the user and get a list of additional components + * + * @return array Components the user wants to use. + */ + public function doComponents() + { + $components = ['Paginator']; + return array_merge($components, $this->_doPropertyChoices( + __d('cake_console', "Would you like this controller to use other components\nbesides PaginatorComponent?"), + __d('cake_console', "Please provide a comma separated list of the component names you'd like to use.\nExample: 'Acl, Security, RequestHandler'") + )); + } + + /** + * Bake scaffold actions + * + * @param string $controllerName Controller name + * @param string $admin Admin route to use + * @param bool $wannaUseSession Set to true to use sessions, false otherwise + * @return string Baked actions + */ + public function bakeActions($controllerName, $admin = null, $wannaUseSession = true) + { + $currentModelName = $modelImport = $this->_modelName($controllerName); + $plugin = $this->plugin; + if ($plugin) { + $plugin .= '.'; + } + App::uses($modelImport, $plugin . 'Model'); + if (!class_exists($modelImport)) { + $this->err(__d('cake_console', 'You must have a model for this class to build basic methods. Please try again.')); + return $this->_stop(); + } + + $modelObj = ClassRegistry::init($currentModelName); + $controllerPath = $this->_controllerPath($controllerName); + $pluralName = $this->_pluralName($currentModelName); + $singularName = Inflector::variable($currentModelName); + $singularHumanName = $this->_singularHumanName($controllerName); + $pluralHumanName = $this->_pluralName($controllerName); + $displayField = $modelObj->displayField; + $primaryKey = $modelObj->primaryKey; + + $this->Template->set(compact( + 'plugin', 'admin', 'controllerPath', 'pluralName', 'singularName', + 'singularHumanName', 'pluralHumanName', 'modelObj', 'wannaUseSession', 'currentModelName', + 'displayField', 'primaryKey' + )); + $actions = $this->Template->generate('actions', 'controller_actions'); + return $actions; + } + + /** + * Confirm a to be baked controller with the user + * + * @param string $controllerName The name of the controller. + * @param string $useDynamicScaffold Whether or not to use dynamic scaffolds. + * @param array $helpers The list of helpers to include. + * @param array $components The list of components to include. + * @return void + */ + public function confirmController($controllerName, $useDynamicScaffold, $helpers, $components) + { + $this->out(); + $this->hr(); + $this->out(__d('cake_console', 'The following controller will be created:')); + $this->hr(); + $this->out(__d('cake_console', "Controller Name:\n\t%s", $controllerName)); + + if (strtolower($useDynamicScaffold) === 'y') { + $this->out("public \$scaffold;"); + } + + $properties = [ + 'helpers' => __d('cake_console', 'Helpers:'), + 'components' => __d('cake_console', 'Components:'), + ]; + + foreach ($properties as $var => $title) { + if (count(${$var})) { + $output = ''; + $length = count(${$var}); + foreach (${$var} as $i => $propElement) { + if ($i != $length - 1) { + $output .= ucfirst($propElement) . ', '; + } else { + $output .= ucfirst($propElement); + } + } + $this->out($title . "\n\t" . $output); + } + } + $this->hr(); + } + + /** + * Assembles and writes a Controller file + * + * @param string $controllerName Controller name already pluralized and correctly cased. + * @param string $actions Actions to add, or set the whole controller to use $scaffold (set $actions to 'scaffold') + * @param array $helpers Helpers to use in controller + * @param array $components Components to use in controller + * @return string Baked controller + */ + public function bake($controllerName, $actions = '', $helpers = null, $components = null) + { + $this->out("\n" . __d('cake_console', 'Baking controller class for %s...', $controllerName), 1, Shell::QUIET); + + if ($helpers === null) { + $helpers = []; + } + if ($components === null) { + $components = []; + } + $isScaffold = ($actions === 'scaffold') ? true : false; + + $this->Template->set([ + 'plugin' => $this->plugin, + 'pluginPath' => empty($this->plugin) ? '' : $this->plugin . '.' + ]); + + if (!in_array('Paginator', (array)$components)) { + $components[] = 'Paginator'; + } + + $this->Template->set(compact('controllerName', 'actions', 'helpers', 'components', 'isScaffold')); + $contents = $this->Template->generate('classes', 'controller'); + + $path = $this->getPath(); + $filename = $path . $controllerName . 'Controller.php'; + if ($this->createFile($filename, $contents)) { + return $contents; + } + return false; + } + + /** + * Assembles and writes a unit test file + * + * @param string $className Controller class name + * @return string Baked test + */ + public function bakeTest($className) + { + $this->Test->plugin = $this->plugin; + $this->Test->connection = $this->connection; + $this->Test->interactive = $this->interactive; + return $this->Test->bake('Controller', $className); + } + + /** + * Bake All the controllers at once. Will only bake controllers for models that exist. + * + * @return void + */ + public function all() + { + $this->interactive = false; + $this->listAll($this->connection, false); + ClassRegistry::config('Model', ['ds' => $this->connection]); + $unitTestExists = $this->_checkUnitTest(); + + $admin = false; + if (!empty($this->params['admin'])) { + $admin = $this->Project->getPrefix(); + } + + $controllersCreated = 0; + foreach ($this->__tables as $table) { + $model = $this->_modelName($table); + $controller = $this->_controllerName($model); + App::uses($model, 'Model'); + if (class_exists($model)) { + $actions = $this->bakeActions($controller); + if ($admin) { + $this->out(__d('cake_console', 'Adding %s methods', $admin)); + $actions .= "\n" . $this->bakeActions($controller, $admin); + } + if ($this->bake($controller, $actions) && $unitTestExists) { + $this->bakeTest($controller); + } + $controllersCreated++; + } + } + + if (!$controllersCreated) { + $this->out(__d('cake_console', 'No Controllers were baked, Models need to exist before Controllers can be baked.')); + } + } + + /** + * Gets the option parser instance and configures it. + * + * @return ConsoleOptionParser + */ + public function getOptionParser() + { + $parser = parent::getOptionParser(); + + $parser->description( + __d('cake_console', 'Bake a controller for a model. Using options you can bake public, admin or both.' + ))->addArgument('name', [ + 'help' => __d('cake_console', 'Name of the controller to bake. Can use Plugin.name to bake controllers into plugins.') + ])->addOption('public', [ + 'help' => __d('cake_console', 'Bake a controller with basic crud actions (index, view, add, edit, delete).'), + 'boolean' => true + ])->addOption('admin', [ + 'help' => __d('cake_console', 'Bake a controller with crud actions for one of the Routing.prefixes.'), + 'boolean' => true + ])->addOption('plugin', [ + 'short' => 'p', + 'help' => __d('cake_console', 'Plugin to bake the controller into.') + ])->addOption('connection', [ + 'short' => 'c', + 'help' => __d('cake_console', 'The connection the controller\'s model is on.') + ])->addOption('theme', [ + 'short' => 't', + 'help' => __d('cake_console', 'Theme to use when baking code.') + ])->addOption('force', [ + 'short' => 'f', + 'help' => __d('cake_console', 'Force overwriting existing files without prompting.') + ])->addSubcommand('all', [ + 'help' => __d('cake_console', 'Bake all controllers with CRUD methods.') + ])->epilog( + __d('cake_console', 'Omitting all arguments and options will enter into an interactive mode.') + ); + + return $parser; + } } diff --git a/lib/Cake/Console/Command/Task/DbConfigTask.php b/lib/Cake/Console/Command/Task/DbConfigTask.php index c76b87bc..38565615 100755 --- a/lib/Cake/Console/Command/Task/DbConfigTask.php +++ b/lib/Cake/Console/Command/Task/DbConfigTask.php @@ -22,364 +22,370 @@ * * @package Cake.Console.Command.Task */ -class DbConfigTask extends AppShell { - -/** - * path to CONFIG directory - * - * @var string - */ - public $path = null; - -/** - * Default configuration settings to use - * - * @var array - */ - protected $_defaultConfig = array( - 'name' => 'default', - 'datasource' => 'Database/Mysql', - 'persistent' => 'false', - 'host' => 'localhost', - 'login' => 'root', - 'password' => 'password', - 'database' => 'project_name', - 'schema' => null, - 'prefix' => null, - 'encoding' => null, - 'port' => null - ); - -/** - * String name of the database config class name. - * Used for testing. - * - * @var string - */ - public $databaseClassName = 'DATABASE_CONFIG'; - -/** - * initialization callback - * - * @return void - */ - public function initialize() { - $this->path = CONFIG; - } - -/** - * Execution method always used for tasks - * - * @return void - */ - public function execute() { - if (empty($this->args)) { - $this->_interactive(); - return $this->_stop(); - } - } - -/** - * Interactive interface - * - * @return void - */ - protected function _interactive() { - $this->hr(); - $this->out(__d('cake_console', 'Database Configuration:')); - $this->hr(); - $done = false; - $dbConfigs = array(); - - while (!$done) { - $name = ''; - - while (!$name) { - $name = $this->in(__d('cake_console', "Name:"), null, 'default'); - if (preg_match('/[^a-z0-9_]/i', $name)) { - $name = ''; - $this->out(__d('cake_console', 'The name may only contain unaccented latin characters, numbers or underscores')); - } elseif (preg_match('/^[^a-z_]/i', $name)) { - $name = ''; - $this->out(__d('cake_console', 'The name must start with an unaccented latin character or an underscore')); - } - } - - $datasource = $this->in(__d('cake_console', 'Datasource:'), array('Mysql', 'Postgres', 'Sqlite', 'Sqlserver'), 'Mysql'); - - $persistent = $this->in(__d('cake_console', 'Persistent Connection?'), array('y', 'n'), 'n'); - if (strtolower($persistent) === 'n') { - $persistent = 'false'; - } else { - $persistent = 'true'; - } - - $host = ''; - while (!$host) { - $host = $this->in(__d('cake_console', 'Database Host:'), null, 'localhost'); - } - - $port = ''; - while (!$port) { - $port = $this->in(__d('cake_console', 'Port?'), null, 'n'); - } - - if (strtolower($port) === 'n') { - $port = null; - } - - $login = ''; - while (!$login) { - $login = $this->in(__d('cake_console', 'User:'), null, 'root'); - } - $password = ''; - $blankPassword = false; - - while (!$password && !$blankPassword) { - $password = $this->in(__d('cake_console', 'Password:')); - - if (!$password) { - $blank = $this->in(__d('cake_console', 'The password you supplied was empty. Use an empty password?'), array('y', 'n'), 'n'); - if ($blank === 'y') { - $blankPassword = true; - } - } - } - - $database = ''; - while (!$database) { - $database = $this->in(__d('cake_console', 'Database Name:'), null, 'cake'); - } - - $prefix = ''; - while (!$prefix) { - $prefix = $this->in(__d('cake_console', 'Table Prefix?'), null, 'n'); - } - if (strtolower($prefix) === 'n') { - $prefix = null; - } - - $encoding = ''; - while (!$encoding) { - $encoding = $this->in(__d('cake_console', 'Table encoding?'), null, 'n'); - } - if (strtolower($encoding) === 'n') { - $encoding = null; - } - - $schema = ''; - if ($datasource === 'postgres') { - while (!$schema) { - $schema = $this->in(__d('cake_console', 'Table schema?'), null, 'n'); - } - } - if (strtolower($schema) === 'n') { - $schema = null; - } - - $config = compact('name', 'datasource', 'persistent', 'host', 'login', 'password', 'database', 'prefix', 'encoding', 'port', 'schema'); - - while (!$this->_verify($config)) { - $this->_interactive(); - } - - $dbConfigs[] = $config; - $doneYet = $this->in(__d('cake_console', 'Do you wish to add another database configuration?'), null, 'n'); - - if (strtolower($doneYet === 'n')) { - $done = true; - } - } - - $this->bake($dbConfigs); - config('database'); - return true; - } - -/** - * Output verification message and bake if it looks good - * - * @param array $config The config data. - * @return bool True if user says it looks good, false otherwise - */ - protected function _verify($config) { - $config += $this->_defaultConfig; - extract($config); - $this->out(); - $this->hr(); - $this->out(__d('cake_console', 'The following database configuration will be created:')); - $this->hr(); - $this->out(__d('cake_console', "Name: %s", $name)); - $this->out(__d('cake_console', "Datasource: %s", $datasource)); - $this->out(__d('cake_console', "Persistent: %s", $persistent)); - $this->out(__d('cake_console', "Host: %s", $host)); - - if ($port) { - $this->out(__d('cake_console', "Port: %s", $port)); - } - - $this->out(__d('cake_console', "User: %s", $login)); - $this->out(__d('cake_console', "Pass: %s", str_repeat('*', strlen($password)))); - $this->out(__d('cake_console', "Database: %s", $database)); - - if ($prefix) { - $this->out(__d('cake_console', "Table prefix: %s", $prefix)); - } - - if ($schema) { - $this->out(__d('cake_console', "Schema: %s", $schema)); - } - - if ($encoding) { - $this->out(__d('cake_console', "Encoding: %s", $encoding)); - } - - $this->hr(); - $looksGood = $this->in(__d('cake_console', 'Look okay?'), array('y', 'n'), 'y'); - - if (strtolower($looksGood) === 'y') { - return $config; - } - return false; - } - -/** - * Assembles and writes database.php - * - * @param array $configs Configuration settings to use - * @return bool Success - */ - public function bake($configs) { - if (!is_dir($this->path)) { - $this->err(__d('cake_console', '%s not found', $this->path)); - return false; - } - - $filename = $this->path . 'database.php'; - $oldConfigs = array(); - - if (file_exists($filename)) { - config('database'); - $db = new $this->databaseClassName; - $temp = get_class_vars(get_class($db)); - - foreach ($temp as $configName => $info) { - $info += $this->_defaultConfig; - - if (!isset($info['schema'])) { - $info['schema'] = null; - } - if (!isset($info['encoding'])) { - $info['encoding'] = null; - } - if (!isset($info['port'])) { - $info['port'] = null; - } - - $info['persistent'] = var_export((bool)$info['persistent'], true); - - $oldConfigs[] = array( - 'name' => $configName, - 'datasource' => $info['datasource'], - 'persistent' => $info['persistent'], - 'host' => $info['host'], - 'port' => $info['port'], - 'login' => $info['login'], - 'password' => $info['password'], - 'database' => $info['database'], - 'prefix' => $info['prefix'], - 'schema' => $info['schema'], - 'encoding' => $info['encoding'] - ); - } - } - - foreach ($oldConfigs as $key => $oldConfig) { - foreach ($configs as $config) { - if ($oldConfig['name'] === $config['name']) { - unset($oldConfigs[$key]); - } - } - } - - $configs = array_merge($oldConfigs, $configs); - $out = "_defaultConfig; - extract($config); - - if (strpos($datasource, 'Database/') === false) { - $datasource = "Database/{$datasource}"; - } - $out .= "\tpublic \${$name} = array(\n"; - $out .= "\t\t'datasource' => '{$datasource}',\n"; - $out .= "\t\t'persistent' => {$persistent},\n"; - $out .= "\t\t'host' => '{$host}',\n"; - - if ($port) { - $out .= "\t\t'port' => {$port},\n"; - } - - $out .= "\t\t'login' => '{$login}',\n"; - $out .= "\t\t'password' => '{$password}',\n"; - $out .= "\t\t'database' => '{$database}',\n"; - - if ($schema) { - $out .= "\t\t'schema' => '{$schema}',\n"; - } - - if ($prefix) { - $out .= "\t\t'prefix' => '{$prefix}',\n"; - } - - if ($encoding) { - $out .= "\t\t'encoding' => '{$encoding}'\n"; - } - - $out .= "\t);\n"; - } - - $out .= "}\n"; - $filename = $this->path . 'database.php'; - return $this->createFile($filename, $out); - } - -/** - * Get a user specified Connection name - * - * @return void - */ - public function getConfig() { - App::uses('ConnectionManager', 'Model'); - $configs = ConnectionManager::enumConnectionObjects(); - - $useDbConfig = key($configs); - if (!is_array($configs) || empty($configs)) { - return $this->execute(); - } - $connections = array_keys($configs); - - if (count($connections) > 1) { - $useDbConfig = $this->in(__d('cake_console', 'Use Database Config') . ':', $connections, $useDbConfig); - } - return $useDbConfig; - } - -/** - * Gets the option parser instance and configures it. - * - * @return ConsoleOptionParser - */ - public function getOptionParser() { - $parser = parent::getOptionParser(); - - $parser->description( - __d('cake_console', 'Bake new database configuration settings.') - ); - - return $parser; - } +class DbConfigTask extends AppShell +{ + + /** + * path to CONFIG directory + * + * @var string + */ + public $path = null; + /** + * String name of the database config class name. + * Used for testing. + * + * @var string + */ + public $databaseClassName = 'DATABASE_CONFIG'; + /** + * Default configuration settings to use + * + * @var array + */ + protected $_defaultConfig = [ + 'name' => 'default', + 'datasource' => 'Database/Mysql', + 'persistent' => 'false', + 'host' => 'localhost', + 'login' => 'root', + 'password' => 'password', + 'database' => 'project_name', + 'schema' => null, + 'prefix' => null, + 'encoding' => null, + 'port' => null + ]; + + /** + * initialization callback + * + * @return void + */ + public function initialize() + { + $this->path = CONFIG; + } + + /** + * Get a user specified Connection name + * + * @return void + */ + public function getConfig() + { + App::uses('ConnectionManager', 'Model'); + $configs = ConnectionManager::enumConnectionObjects(); + + $useDbConfig = key($configs); + if (!is_array($configs) || empty($configs)) { + return $this->execute(); + } + $connections = array_keys($configs); + + if (count($connections) > 1) { + $useDbConfig = $this->in(__d('cake_console', 'Use Database Config') . ':', $connections, $useDbConfig); + } + return $useDbConfig; + } + + /** + * Execution method always used for tasks + * + * @return void + */ + public function execute() + { + if (empty($this->args)) { + $this->_interactive(); + return $this->_stop(); + } + } + + /** + * Interactive interface + * + * @return void + */ + protected function _interactive() + { + $this->hr(); + $this->out(__d('cake_console', 'Database Configuration:')); + $this->hr(); + $done = false; + $dbConfigs = []; + + while (!$done) { + $name = ''; + + while (!$name) { + $name = $this->in(__d('cake_console', "Name:"), null, 'default'); + if (preg_match('/[^a-z0-9_]/i', $name)) { + $name = ''; + $this->out(__d('cake_console', 'The name may only contain unaccented latin characters, numbers or underscores')); + } else if (preg_match('/^[^a-z_]/i', $name)) { + $name = ''; + $this->out(__d('cake_console', 'The name must start with an unaccented latin character or an underscore')); + } + } + + $datasource = $this->in(__d('cake_console', 'Datasource:'), ['Mysql', 'Postgres', 'Sqlite', 'Sqlserver'], 'Mysql'); + + $persistent = $this->in(__d('cake_console', 'Persistent Connection?'), ['y', 'n'], 'n'); + if (strtolower($persistent) === 'n') { + $persistent = 'false'; + } else { + $persistent = 'true'; + } + + $host = ''; + while (!$host) { + $host = $this->in(__d('cake_console', 'Database Host:'), null, 'localhost'); + } + + $port = ''; + while (!$port) { + $port = $this->in(__d('cake_console', 'Port?'), null, 'n'); + } + + if (strtolower($port) === 'n') { + $port = null; + } + + $login = ''; + while (!$login) { + $login = $this->in(__d('cake_console', 'User:'), null, 'root'); + } + $password = ''; + $blankPassword = false; + + while (!$password && !$blankPassword) { + $password = $this->in(__d('cake_console', 'Password:')); + + if (!$password) { + $blank = $this->in(__d('cake_console', 'The password you supplied was empty. Use an empty password?'), ['y', 'n'], 'n'); + if ($blank === 'y') { + $blankPassword = true; + } + } + } + + $database = ''; + while (!$database) { + $database = $this->in(__d('cake_console', 'Database Name:'), null, 'cake'); + } + + $prefix = ''; + while (!$prefix) { + $prefix = $this->in(__d('cake_console', 'Table Prefix?'), null, 'n'); + } + if (strtolower($prefix) === 'n') { + $prefix = null; + } + + $encoding = ''; + while (!$encoding) { + $encoding = $this->in(__d('cake_console', 'Table encoding?'), null, 'n'); + } + if (strtolower($encoding) === 'n') { + $encoding = null; + } + + $schema = ''; + if ($datasource === 'postgres') { + while (!$schema) { + $schema = $this->in(__d('cake_console', 'Table schema?'), null, 'n'); + } + } + if (strtolower($schema) === 'n') { + $schema = null; + } + + $config = compact('name', 'datasource', 'persistent', 'host', 'login', 'password', 'database', 'prefix', 'encoding', 'port', 'schema'); + + while (!$this->_verify($config)) { + $this->_interactive(); + } + + $dbConfigs[] = $config; + $doneYet = $this->in(__d('cake_console', 'Do you wish to add another database configuration?'), null, 'n'); + + if (strtolower($doneYet === 'n')) { + $done = true; + } + } + + $this->bake($dbConfigs); + config('database'); + return true; + } + + /** + * Output verification message and bake if it looks good + * + * @param array $config The config data. + * @return bool True if user says it looks good, false otherwise + */ + protected function _verify($config) + { + $config += $this->_defaultConfig; + extract($config); + $this->out(); + $this->hr(); + $this->out(__d('cake_console', 'The following database configuration will be created:')); + $this->hr(); + $this->out(__d('cake_console', "Name: %s", $name)); + $this->out(__d('cake_console', "Datasource: %s", $datasource)); + $this->out(__d('cake_console', "Persistent: %s", $persistent)); + $this->out(__d('cake_console', "Host: %s", $host)); + + if ($port) { + $this->out(__d('cake_console', "Port: %s", $port)); + } + + $this->out(__d('cake_console', "User: %s", $login)); + $this->out(__d('cake_console', "Pass: %s", str_repeat('*', strlen($password)))); + $this->out(__d('cake_console', "Database: %s", $database)); + + if ($prefix) { + $this->out(__d('cake_console', "Table prefix: %s", $prefix)); + } + + if ($schema) { + $this->out(__d('cake_console', "Schema: %s", $schema)); + } + + if ($encoding) { + $this->out(__d('cake_console', "Encoding: %s", $encoding)); + } + + $this->hr(); + $looksGood = $this->in(__d('cake_console', 'Look okay?'), ['y', 'n'], 'y'); + + if (strtolower($looksGood) === 'y') { + return $config; + } + return false; + } + + /** + * Assembles and writes database.php + * + * @param array $configs Configuration settings to use + * @return bool Success + */ + public function bake($configs) + { + if (!is_dir($this->path)) { + $this->err(__d('cake_console', '%s not found', $this->path)); + return false; + } + + $filename = $this->path . 'database.php'; + $oldConfigs = []; + + if (file_exists($filename)) { + config('database'); + $db = new $this->databaseClassName; + $temp = get_class_vars(get_class($db)); + + foreach ($temp as $configName => $info) { + $info += $this->_defaultConfig; + + if (!isset($info['schema'])) { + $info['schema'] = null; + } + if (!isset($info['encoding'])) { + $info['encoding'] = null; + } + if (!isset($info['port'])) { + $info['port'] = null; + } + + $info['persistent'] = var_export((bool)$info['persistent'], true); + + $oldConfigs[] = [ + 'name' => $configName, + 'datasource' => $info['datasource'], + 'persistent' => $info['persistent'], + 'host' => $info['host'], + 'port' => $info['port'], + 'login' => $info['login'], + 'password' => $info['password'], + 'database' => $info['database'], + 'prefix' => $info['prefix'], + 'schema' => $info['schema'], + 'encoding' => $info['encoding'] + ]; + } + } + + foreach ($oldConfigs as $key => $oldConfig) { + foreach ($configs as $config) { + if ($oldConfig['name'] === $config['name']) { + unset($oldConfigs[$key]); + } + } + } + + $configs = array_merge($oldConfigs, $configs); + $out = "_defaultConfig; + extract($config); + + if (strpos($datasource, 'Database/') === false) { + $datasource = "Database/{$datasource}"; + } + $out .= "\tpublic \${$name} = array(\n"; + $out .= "\t\t'datasource' => '{$datasource}',\n"; + $out .= "\t\t'persistent' => {$persistent},\n"; + $out .= "\t\t'host' => '{$host}',\n"; + + if ($port) { + $out .= "\t\t'port' => {$port},\n"; + } + + $out .= "\t\t'login' => '{$login}',\n"; + $out .= "\t\t'password' => '{$password}',\n"; + $out .= "\t\t'database' => '{$database}',\n"; + + if ($schema) { + $out .= "\t\t'schema' => '{$schema}',\n"; + } + + if ($prefix) { + $out .= "\t\t'prefix' => '{$prefix}',\n"; + } + + if ($encoding) { + $out .= "\t\t'encoding' => '{$encoding}'\n"; + } + + $out .= "\t);\n"; + } + + $out .= "}\n"; + $filename = $this->path . 'database.php'; + return $this->createFile($filename, $out); + } + + /** + * Gets the option parser instance and configures it. + * + * @return ConsoleOptionParser + */ + public function getOptionParser() + { + $parser = parent::getOptionParser(); + + $parser->description( + __d('cake_console', 'Bake new database configuration settings.') + ); + + return $parser; + } } diff --git a/lib/Cake/Console/Command/Task/ExtractTask.php b/lib/Cake/Console/Command/Task/ExtractTask.php index eaa691e3..275883b1 100755 --- a/lib/Cake/Console/Command/Task/ExtractTask.php +++ b/lib/Cake/Console/Command/Task/ExtractTask.php @@ -25,822 +25,843 @@ * * @package Cake.Console.Command.Task */ -class ExtractTask extends AppShell { - -/** - * Paths to use when looking for strings - * - * @var string - */ - protected $_paths = array(); - -/** - * Files from where to extract - * - * @var array - */ - protected $_files = array(); - -/** - * Merge all domain and category strings into the default.pot file - * - * @var bool - */ - protected $_merge = false; - -/** - * Current file being processed - * - * @var string - */ - protected $_file = null; - -/** - * Contains all content waiting to be write - * - * @var string - */ - protected $_storage = array(); - -/** - * Extracted tokens - * - * @var array - */ - protected $_tokens = array(); - -/** - * Extracted strings indexed by category, domain, msgid and context. - * - * @var array - */ - protected $_translations = array(); - -/** - * Destination path - * - * @var string - */ - protected $_output = null; - -/** - * An array of directories to exclude. - * - * @var array - */ - protected $_exclude = array(); - -/** - * Holds whether this call should extract model validation messages - * - * @var bool - */ - protected $_extractValidation = true; - -/** - * Holds the validation string domain to use for validation messages when extracting - * - * @var bool - */ - protected $_validationDomain = 'default'; - -/** - * Holds whether this call should extract the CakePHP Lib messages - * - * @var bool - */ - protected $_extractCore = false; - -/** - * Method to interact with the User and get path selections. - * - * @return void - */ - protected function _getPaths() { - $defaultPath = APP; - while (true) { - $currentPaths = count($this->_paths) > 0 ? $this->_paths : array('None'); - $message = __d( - 'cake_console', - "Current paths: %s\nWhat is the path you would like to extract?\n[Q]uit [D]one", - implode(', ', $currentPaths) - ); - $response = $this->in($message, null, $defaultPath); - if (strtoupper($response) === 'Q') { - $this->err(__d('cake_console', 'Extract Aborted')); - return $this->_stop(); - } elseif (strtoupper($response) === 'D' && count($this->_paths)) { - $this->out(); - return; - } elseif (strtoupper($response) === 'D') { - $this->err(__d('cake_console', 'No directories selected. Please choose a directory.')); - } elseif (is_dir($response)) { - $this->_paths[] = $response; - $defaultPath = 'D'; - } else { - $this->err(__d('cake_console', 'The directory path you supplied was not found. Please try again.')); - } - $this->out(); - } - } - -/** - * Execution method always used for tasks - * - * @return void - */ - public function execute() { - if (!empty($this->params['exclude'])) { - $this->_exclude = explode(',', str_replace('/', DS, $this->params['exclude'])); - } - if (isset($this->params['files']) && !is_array($this->params['files'])) { - $this->_files = explode(',', $this->params['files']); - } - if (isset($this->params['paths'])) { - $this->_paths = explode(',', $this->params['paths']); - } elseif (isset($this->params['plugin'])) { - $plugin = Inflector::camelize($this->params['plugin']); - if (!CakePlugin::loaded($plugin)) { - CakePlugin::load($plugin); - } - $this->_paths = array(CakePlugin::path($plugin)); - $this->params['plugin'] = $plugin; - } else { - $this->_getPaths(); - } - - if (isset($this->params['extract-core'])) { - $this->_extractCore = !(strtolower($this->params['extract-core']) === 'no'); - } else { - $response = $this->in(__d('cake_console', 'Would you like to extract the messages from the CakePHP core?'), array('y', 'n'), 'n'); - $this->_extractCore = strtolower($response) === 'y'; - } - - if (!empty($this->params['exclude-plugins']) && $this->_isExtractingApp()) { - $this->_exclude = array_merge($this->_exclude, App::path('plugins')); - } - - if (!empty($this->params['ignore-model-validation']) || (!$this->_isExtractingApp() && empty($plugin))) { - $this->_extractValidation = false; - } - if (!empty($this->params['validation-domain'])) { - $this->_validationDomain = $this->params['validation-domain']; - } - - if ($this->_extractCore) { - $this->_paths[] = CAKE; - $this->_exclude = array_merge($this->_exclude, array( - CAKE . 'Test', - CAKE . 'Console' . DS . 'Templates' - )); - } - - if (isset($this->params['output'])) { - $this->_output = $this->params['output']; - } elseif (isset($this->params['plugin'])) { - $this->_output = $this->_paths[0] . DS . 'Locale'; - } else { - $message = __d('cake_console', "What is the path you would like to output?\n[Q]uit", $this->_paths[0] . DS . 'Locale'); - while (true) { - $response = $this->in($message, null, rtrim($this->_paths[0], DS) . DS . 'Locale'); - if (strtoupper($response) === 'Q') { - $this->err(__d('cake_console', 'Extract Aborted')); - return $this->_stop(); - } elseif ($this->_isPathUsable($response)) { - $this->_output = $response . DS; - break; - } else { - $this->err(__d('cake_console', 'The directory path you supplied was not found. Please try again.')); - } - $this->out(); - } - } - - if (isset($this->params['merge'])) { - $this->_merge = !(strtolower($this->params['merge']) === 'no'); - } else { - $this->out(); - $response = $this->in(__d('cake_console', 'Would you like to merge all domain and category strings into the default.pot file?'), array('y', 'n'), 'n'); - $this->_merge = strtolower($response) === 'y'; - } - - if (empty($this->_files)) { - $this->_searchFiles(); - } - - $this->_output = rtrim($this->_output, DS) . DS; - if (!$this->_isPathUsable($this->_output)) { - $this->err(__d('cake_console', 'The output directory %s was not found or writable.', $this->_output)); - return $this->_stop(); - } - - $this->_extract(); - } - -/** - * Add a translation to the internal translations property - * - * Takes care of duplicate translations - * - * @param string $category The category - * @param string $domain The domain - * @param string $msgid The message string - * @param array $details The file and line references - * @return void - */ - protected function _addTranslation($category, $domain, $msgid, $details = array()) { - $context = ''; - if (isset($details['msgctxt'])) { - $context = $details['msgctxt']; - } - - if (empty($this->_translations[$category][$domain][$msgid][$context])) { - $this->_translations[$category][$domain][$msgid][$context] = array( - 'msgid_plural' => false, - ); - } - - if (isset($details['msgid_plural'])) { - $this->_translations[$category][$domain][$msgid][$context]['msgid_plural'] = $details['msgid_plural']; - } - if (isset($details['file'])) { - $line = 0; - if (isset($details['line'])) { - $line = $details['line']; - } - $this->_translations[$category][$domain][$msgid][$context]['references'][$details['file']][] = $line; - } - } - -/** - * Extract text - * - * @return void - */ - protected function _extract() { - $this->out(); - $this->out(); - $this->out(__d('cake_console', 'Extracting...')); - $this->hr(); - $this->out(__d('cake_console', 'Paths:')); - foreach ($this->_paths as $path) { - $this->out(' ' . $path); - } - $this->out(__d('cake_console', 'Output Directory: ') . $this->_output); - $this->hr(); - $this->_extractTokens(); - $this->_extractValidationMessages(); - $this->_buildFiles(); - $this->_writeFiles(); - $this->_paths = $this->_files = $this->_storage = array(); - $this->_translations = $this->_tokens = array(); - $this->_extractValidation = true; - $this->out(); - $this->out(__d('cake_console', 'Done.')); - } - -/** - * Gets the option parser instance and configures it. - * - * @return ConsoleOptionParser - */ - public function getOptionParser() { - $parser = parent::getOptionParser(); - - $parser->description( - __d('cake_console', 'CakePHP Language String Extraction:') - )->addOption('app', array( - 'help' => __d('cake_console', 'Directory where your application is located.') - ))->addOption('paths', array( - 'help' => __d('cake_console', 'Comma separated list of paths.') - ))->addOption('merge', array( - 'help' => __d('cake_console', 'Merge all domain and category strings into the default.po file.'), - 'choices' => array('yes', 'no') - ))->addOption('no-location', array( - 'boolean' => true, - 'default' => false, - 'help' => __d('cake_console', 'Do not write lines with locations'), - ))->addOption('output', array( - 'help' => __d('cake_console', 'Full path to output directory.') - ))->addOption('files', array( - 'help' => __d('cake_console', 'Comma separated list of files.') - ))->addOption('exclude-plugins', array( - 'boolean' => true, - 'default' => true, - 'help' => __d('cake_console', 'Ignores all files in plugins if this command is run inside from the same app directory.') - ))->addOption('plugin', array( - 'help' => __d('cake_console', 'Extracts tokens only from the plugin specified and puts the result in the plugin\'s Locale directory.') - ))->addOption('ignore-model-validation', array( - 'boolean' => true, - 'default' => false, - 'help' => __d('cake_console', 'Ignores validation messages in the $validate property.' . - ' If this flag is not set and the command is run from the same app directory,' . - ' all messages in model validation rules will be extracted as tokens.' - ) - ))->addOption('validation-domain', array( - 'help' => __d('cake_console', 'If set to a value, the localization domain to be used for model validation messages.') - ))->addOption('exclude', array( - 'help' => __d('cake_console', 'Comma separated list of directories to exclude.' . - ' Any path containing a path segment with the provided values will be skipped. E.g. test,vendors' - ) - ))->addOption('overwrite', array( - 'boolean' => true, - 'default' => false, - 'help' => __d('cake_console', 'Always overwrite existing .pot files.') - ))->addOption('extract-core', array( - 'help' => __d('cake_console', 'Extract messages from the CakePHP core libs.'), - 'choices' => array('yes', 'no') - )); - - return $parser; - } - -/** - * Extract tokens out of all files to be processed - * - * @return void - */ - protected function _extractTokens() { - foreach ($this->_files as $file) { - $this->_file = $file; - $this->out(__d('cake_console', 'Processing %s...', $file), 1, Shell::VERBOSE); - - $code = file_get_contents($file); - $allTokens = token_get_all($code); - - $this->_tokens = array(); - foreach ($allTokens as $token) { - if (!is_array($token) || ($token[0] !== T_WHITESPACE && $token[0] !== T_INLINE_HTML)) { - $this->_tokens[] = $token; - } - } - unset($allTokens); - $this->_parse('__', array('singular')); - $this->_parse('__n', array('singular', 'plural')); - $this->_parse('__d', array('domain', 'singular')); - $this->_parse('__c', array('singular', 'category')); - $this->_parse('__dc', array('domain', 'singular', 'category')); - $this->_parse('__dn', array('domain', 'singular', 'plural')); - $this->_parse('__dcn', array('domain', 'singular', 'plural', 'count', 'category')); - - $this->_parse('__x', array('context', 'singular')); - $this->_parse('__xn', array('context', 'singular', 'plural')); - $this->_parse('__dx', array('domain', 'context', 'singular')); - $this->_parse('__dxc', array('domain', 'context', 'singular', 'category')); - $this->_parse('__dxn', array('domain', 'context', 'singular', 'plural')); - $this->_parse('__dxcn', array('domain', 'context', 'singular', 'plural', 'count', 'category')); - $this->_parse('__xc', array('context', 'singular', 'category')); - - } - } - -/** - * Parse tokens - * - * @param string $functionName Function name that indicates translatable string (e.g: '__') - * @param array $map Array containing what variables it will find (e.g: category, domain, singular, plural) - * @return void - */ - protected function _parse($functionName, $map) { - $count = 0; - $categories = array('LC_ALL', 'LC_COLLATE', 'LC_CTYPE', 'LC_MONETARY', 'LC_NUMERIC', 'LC_TIME', 'LC_MESSAGES'); - $tokenCount = count($this->_tokens); - - while (($tokenCount - $count) > 1) { - $countToken = $this->_tokens[$count]; - $firstParenthesis = $this->_tokens[$count + 1]; - if (!is_array($countToken)) { - $count++; - continue; - } - - list($type, $string, $line) = $countToken; - if (($type == T_STRING) && ($string === $functionName) && ($firstParenthesis === '(')) { - $position = $count; - $depth = 0; - - while (!$depth) { - if ($this->_tokens[$position] === '(') { - $depth++; - } elseif ($this->_tokens[$position] === ')') { - $depth--; - } - $position++; - } - - $mapCount = count($map); - $strings = $this->_getStrings($position, $mapCount); - - if ($mapCount === count($strings)) { - extract(array_combine($map, $strings)); - $category = isset($category) ? $category : 6; - $category = (int)$category; - $categoryName = $categories[$category]; - - $domain = isset($domain) ? $domain : 'default'; - $details = array( - 'file' => $this->_file, - 'line' => $line, - ); - if (isset($plural)) { - $details['msgid_plural'] = $plural; - } - if (isset($context)) { - $details['msgctxt'] = $context; - } - // Skip LC_TIME files as we use a special file format for them. - if ($categoryName !== 'LC_TIME') { - $this->_addTranslation($categoryName, $domain, $singular, $details); - } - } elseif (!is_array($this->_tokens[$count - 1]) || $this->_tokens[$count - 1][0] != T_FUNCTION) { - $this->_markerError($this->_file, $line, $functionName, $count); - } - } - $count++; - } - } - -/** - * Looks for models in the application and extracts the validation messages - * to be added to the translation map - * - * @return void - */ - protected function _extractValidationMessages() { - if (!$this->_extractValidation) { - return; - } - - $plugins = array(null); - if (empty($this->params['exclude-plugins'])) { - $plugins = array_merge($plugins, App::objects('plugin', null, false)); - } - foreach ($plugins as $plugin) { - $this->_extractPluginValidationMessages($plugin); - } - } - -/** - * Extract validation messages from application or plugin models - * - * @param string $plugin Plugin name or `null` to process application models - * @return void - */ - protected function _extractPluginValidationMessages($plugin = null) { - App::uses('AppModel', 'Model'); - if (!empty($plugin)) { - if (!CakePlugin::loaded($plugin)) { - return; - } - App::uses($plugin . 'AppModel', $plugin . '.Model'); - $plugin = $plugin . '.'; - } - $models = App::objects($plugin . 'Model', null, false); - - foreach ($models as $model) { - App::uses($model, $plugin . 'Model'); - $reflection = new ReflectionClass($model); - if (!$reflection->isSubClassOf('Model')) { - continue; - } - $properties = $reflection->getDefaultProperties(); - $validate = $properties['validate']; - if (empty($validate)) { - continue; - } - - $file = $reflection->getFileName(); - $domain = $this->_validationDomain; - if (!empty($properties['validationDomain'])) { - $domain = $properties['validationDomain']; - } - foreach ($validate as $field => $rules) { - $this->_processValidationRules($field, $rules, $file, $domain); - } - } - } - -/** - * Process a validation rule for a field and looks for a message to be added - * to the translation map - * - * @param string $field the name of the field that is being processed - * @param array $rules the set of validation rules for the field - * @param string $file the file name where this validation rule was found - * @param string $domain default domain to bind the validations to - * @param string $category the translation category - * @return void - */ - protected function _processValidationRules($field, $rules, $file, $domain, $category = 'LC_MESSAGES') { - if (!is_array($rules)) { - return; - } - - $dims = Hash::dimensions($rules); - if ($dims === 1 || ($dims === 2 && isset($rules['message']))) { - $rules = array($rules); - } - - foreach ($rules as $rule => $validateProp) { - $msgid = null; - if (isset($validateProp['message'])) { - if (is_array($validateProp['message'])) { - $msgid = $validateProp['message'][0]; - } else { - $msgid = $validateProp['message']; - } - } elseif (is_string($rule)) { - $msgid = $rule; - } - if ($msgid) { - $msgid = $this->_formatString(sprintf("'%s'", $msgid)); - $details = array( - 'file' => $file, - 'line' => 'validation for field ' . $field - ); - $this->_addTranslation($category, $domain, $msgid, $details); - } - } - } - -/** - * Build the translate template file contents out of obtained strings - * - * @return void - */ - protected function _buildFiles() { - $paths = $this->_paths; - $paths[] = realpath(APP) . DS; - - usort($paths, function ($a, $b) { - return strlen($b) - strlen($a); - }); - - foreach ($this->_translations as $category => $domains) { - foreach ($domains as $domain => $translations) { - foreach ($translations as $msgid => $contexts) { - foreach ($contexts as $context => $details) { - $plural = $details['msgid_plural']; - $header = ''; - if (empty($this->params['no-location'])) { - $files = $details['references']; - $occurrences = array(); - foreach ($files as $file => $lines) { - $lines = array_unique($lines); - $occurrences[] = $file . ':' . implode(';', $lines); - } - $occurrences = implode("\n#: ", $occurrences); - $header = '#: ' . str_replace(DS, '/', str_replace($paths, '', $occurrences)) . "\n"; - } - - $sentence = ''; - if ($context) { - $sentence .= "msgctxt \"{$context}\"\n"; - } - if ($plural === false) { - $sentence .= "msgid \"{$msgid}\"\n"; - $sentence .= "msgstr \"\"\n\n"; - } else { - $sentence .= "msgid \"{$msgid}\"\n"; - $sentence .= "msgid_plural \"{$plural}\"\n"; - $sentence .= "msgstr[0] \"\"\n"; - $sentence .= "msgstr[1] \"\"\n\n"; - } - - $this->_store($category, $domain, $header, $sentence); - if (($category !== 'LC_MESSAGES' || $domain !== 'default') && $this->_merge) { - $this->_store('LC_MESSAGES', 'default', $header, $sentence); - } - } - } - } - } - } - -/** - * Prepare a file to be stored - * - * @param string $category The category - * @param string $domain The domain - * @param string $header The header content. - * @param string $sentence The sentence to store. - * @return void - */ - protected function _store($category, $domain, $header, $sentence) { - if (!isset($this->_storage[$category])) { - $this->_storage[$category] = array(); - } - if (!isset($this->_storage[$category][$domain])) { - $this->_storage[$category][$domain] = array(); - } - if (!isset($this->_storage[$category][$domain][$sentence])) { - $this->_storage[$category][$domain][$sentence] = $header; - } else { - $this->_storage[$category][$domain][$sentence] .= $header; - } - } - -/** - * Write the files that need to be stored - * - * @return void - */ - protected function _writeFiles() { - $overwriteAll = false; - if (!empty($this->params['overwrite'])) { - $overwriteAll = true; - } - foreach ($this->_storage as $category => $domains) { - foreach ($domains as $domain => $sentences) { - $output = $this->_writeHeader(); - foreach ($sentences as $sentence => $header) { - $output .= $header . $sentence; - } - - $filename = $domain . '.pot'; - if ($category === 'LC_MESSAGES') { - $File = new File($this->_output . $filename); - } else { - new Folder($this->_output . $category, true); - $File = new File($this->_output . $category . DS . $filename); - } - $response = ''; - while ($overwriteAll === false && $File->exists() && strtoupper($response) !== 'Y') { - $this->out(); - $response = $this->in( - __d('cake_console', 'Error: %s already exists in this location. Overwrite? [Y]es, [N]o, [A]ll', $filename), - array('y', 'n', 'a'), - 'y' - ); - if (strtoupper($response) === 'N') { - $response = ''; - while (!$response) { - $response = $this->in(__d('cake_console', "What would you like to name this file?"), null, 'new_' . $filename); - $File = new File($this->_output . $response); - $filename = $response; - } - } elseif (strtoupper($response) === 'A') { - $overwriteAll = true; - } - } - $File->write($output); - $File->close(); - } - } - } - -/** - * Build the translation template header - * - * @return string Translation template header - */ - protected function _writeHeader() { - $output = "# LANGUAGE translation of CakePHP Application\n"; - $output .= "# Copyright YEAR NAME \n"; - $output .= "#\n"; - $output .= "#, fuzzy\n"; - $output .= "msgid \"\"\n"; - $output .= "msgstr \"\"\n"; - $output .= "\"Project-Id-Version: PROJECT VERSION\\n\"\n"; - $output .= "\"PO-Revision-Date: YYYY-mm-DD HH:MM+ZZZZ\\n\"\n"; - $output .= "\"Last-Translator: NAME \\n\"\n"; - $output .= "\"Language-Team: LANGUAGE \\n\"\n"; - $output .= "\"MIME-Version: 1.0\\n\"\n"; - $output .= "\"Content-Type: text/plain; charset=utf-8\\n\"\n"; - $output .= "\"Content-Transfer-Encoding: 8bit\\n\"\n"; - $output .= "\"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\\n\"\n\n"; - return $output; - } - -/** - * Get the strings from the position forward - * - * @param int &$position Actual position on tokens array - * @param int $target Number of strings to extract - * @return array Strings extracted - */ - protected function _getStrings(&$position, $target) { - $strings = array(); - $count = count($strings); - while ($count < $target && ($this->_tokens[$position] === ',' || $this->_tokens[$position][0] == T_CONSTANT_ENCAPSED_STRING || $this->_tokens[$position][0] == T_LNUMBER)) { - $count = count($strings); - if ($this->_tokens[$position][0] == T_CONSTANT_ENCAPSED_STRING && $this->_tokens[$position + 1] === '.') { - $string = ''; - while ($this->_tokens[$position][0] == T_CONSTANT_ENCAPSED_STRING || $this->_tokens[$position] === '.') { - if ($this->_tokens[$position][0] == T_CONSTANT_ENCAPSED_STRING) { - $string .= $this->_formatString($this->_tokens[$position][1]); - } - $position++; - } - $strings[] = $string; - } elseif ($this->_tokens[$position][0] == T_CONSTANT_ENCAPSED_STRING) { - $strings[] = $this->_formatString($this->_tokens[$position][1]); - } elseif ($this->_tokens[$position][0] == T_LNUMBER) { - $strings[] = $this->_tokens[$position][1]; - } - $position++; - } - return $strings; - } - -/** - * Format a string to be added as a translatable string - * - * @param string $string String to format - * @return string Formatted string - */ - protected function _formatString($string) { - $quote = substr($string, 0, 1); - $string = substr($string, 1, -1); - if ($quote === '"') { - $string = stripcslashes($string); - } else { - $string = strtr($string, array("\\'" => "'", "\\\\" => "\\")); - } - $string = str_replace("\r\n", "\n", $string); - return addcslashes($string, "\0..\37\\\""); - } - -/** - * Indicate an invalid marker on a processed file - * - * @param string $file File where invalid marker resides - * @param int $line Line number - * @param string $marker Marker found - * @param int $count Count - * @return void - */ - protected function _markerError($file, $line, $marker, $count) { - $this->err(__d('cake_console', "Invalid marker content in %s:%s\n* %s(", $file, $line, $marker)); - $count += 2; - $tokenCount = count($this->_tokens); - $parenthesis = 1; - - while ((($tokenCount - $count) > 0) && $parenthesis) { - if (is_array($this->_tokens[$count])) { - $this->err($this->_tokens[$count][1], false); - } else { - $this->err($this->_tokens[$count], false); - if ($this->_tokens[$count] === '(') { - $parenthesis++; - } - - if ($this->_tokens[$count] === ')') { - $parenthesis--; - } - } - $count++; - } - $this->err("\n", true); - } - -/** - * Search files that may contain translatable strings - * - * @return void - */ - protected function _searchFiles() { - $pattern = false; - if (!empty($this->_exclude)) { - $exclude = array(); - foreach ($this->_exclude as $e) { - if (DS !== '\\' && $e[0] !== DS) { - $e = DS . $e; - } - $exclude[] = preg_quote($e, '/'); - } - $pattern = '/' . implode('|', $exclude) . '/'; - } - foreach ($this->_paths as $i => $path) { - $this->_paths[$i] = realpath($path) . DS; - $Folder = new Folder($this->_paths[$i]); - $files = $Folder->findRecursive('.*\.(php|ctp|thtml|inc|tpl)', true); - if (!empty($pattern)) { - $files = preg_grep($pattern, $files, PREG_GREP_INVERT); - $files = array_values($files); - } - $this->_files = array_merge($this->_files, $files); - } - $this->_files = array_unique($this->_files); - } - -/** - * Returns whether this execution is meant to extract string only from directories in folder represented by the - * APP constant, i.e. this task is extracting strings from same application. - * - * @return bool - */ - protected function _isExtractingApp() { - return $this->_paths === array(APP); - } - -/** - * Checks whether or not a given path is usable for writing. - * - * @param string $path Path to folder - * @return bool true if it exists and is writable, false otherwise - */ - protected function _isPathUsable($path) { - return is_dir($path) && is_writable($path); - } +class ExtractTask extends AppShell +{ + + /** + * Paths to use when looking for strings + * + * @var string + */ + protected $_paths = []; + + /** + * Files from where to extract + * + * @var array + */ + protected $_files = []; + + /** + * Merge all domain and category strings into the default.pot file + * + * @var bool + */ + protected $_merge = false; + + /** + * Current file being processed + * + * @var string + */ + protected $_file = null; + + /** + * Contains all content waiting to be write + * + * @var string + */ + protected $_storage = []; + + /** + * Extracted tokens + * + * @var array + */ + protected $_tokens = []; + + /** + * Extracted strings indexed by category, domain, msgid and context. + * + * @var array + */ + protected $_translations = []; + + /** + * Destination path + * + * @var string + */ + protected $_output = null; + + /** + * An array of directories to exclude. + * + * @var array + */ + protected $_exclude = []; + + /** + * Holds whether this call should extract model validation messages + * + * @var bool + */ + protected $_extractValidation = true; + + /** + * Holds the validation string domain to use for validation messages when extracting + * + * @var bool + */ + protected $_validationDomain = 'default'; + + /** + * Holds whether this call should extract the CakePHP Lib messages + * + * @var bool + */ + protected $_extractCore = false; + + /** + * Execution method always used for tasks + * + * @return void + */ + public function execute() + { + if (!empty($this->params['exclude'])) { + $this->_exclude = explode(',', str_replace('/', DS, $this->params['exclude'])); + } + if (isset($this->params['files']) && !is_array($this->params['files'])) { + $this->_files = explode(',', $this->params['files']); + } + if (isset($this->params['paths'])) { + $this->_paths = explode(',', $this->params['paths']); + } else if (isset($this->params['plugin'])) { + $plugin = Inflector::camelize($this->params['plugin']); + if (!CakePlugin::loaded($plugin)) { + CakePlugin::load($plugin); + } + $this->_paths = [CakePlugin::path($plugin)]; + $this->params['plugin'] = $plugin; + } else { + $this->_getPaths(); + } + + if (isset($this->params['extract-core'])) { + $this->_extractCore = !(strtolower($this->params['extract-core']) === 'no'); + } else { + $response = $this->in(__d('cake_console', 'Would you like to extract the messages from the CakePHP core?'), ['y', 'n'], 'n'); + $this->_extractCore = strtolower($response) === 'y'; + } + + if (!empty($this->params['exclude-plugins']) && $this->_isExtractingApp()) { + $this->_exclude = array_merge($this->_exclude, App::path('plugins')); + } + + if (!empty($this->params['ignore-model-validation']) || (!$this->_isExtractingApp() && empty($plugin))) { + $this->_extractValidation = false; + } + if (!empty($this->params['validation-domain'])) { + $this->_validationDomain = $this->params['validation-domain']; + } + + if ($this->_extractCore) { + $this->_paths[] = CAKE; + $this->_exclude = array_merge($this->_exclude, [ + CAKE . 'Test', + CAKE . 'Console' . DS . 'Templates' + ]); + } + + if (isset($this->params['output'])) { + $this->_output = $this->params['output']; + } else if (isset($this->params['plugin'])) { + $this->_output = $this->_paths[0] . DS . 'Locale'; + } else { + $message = __d('cake_console', "What is the path you would like to output?\n[Q]uit", $this->_paths[0] . DS . 'Locale'); + while (true) { + $response = $this->in($message, null, rtrim($this->_paths[0], DS) . DS . 'Locale'); + if (strtoupper($response) === 'Q') { + $this->err(__d('cake_console', 'Extract Aborted')); + return $this->_stop(); + } else if ($this->_isPathUsable($response)) { + $this->_output = $response . DS; + break; + } else { + $this->err(__d('cake_console', 'The directory path you supplied was not found. Please try again.')); + } + $this->out(); + } + } + + if (isset($this->params['merge'])) { + $this->_merge = !(strtolower($this->params['merge']) === 'no'); + } else { + $this->out(); + $response = $this->in(__d('cake_console', 'Would you like to merge all domain and category strings into the default.pot file?'), ['y', 'n'], 'n'); + $this->_merge = strtolower($response) === 'y'; + } + + if (empty($this->_files)) { + $this->_searchFiles(); + } + + $this->_output = rtrim($this->_output, DS) . DS; + if (!$this->_isPathUsable($this->_output)) { + $this->err(__d('cake_console', 'The output directory %s was not found or writable.', $this->_output)); + return $this->_stop(); + } + + $this->_extract(); + } + + /** + * Method to interact with the User and get path selections. + * + * @return void + */ + protected function _getPaths() + { + $defaultPath = APP; + while (true) { + $currentPaths = count($this->_paths) > 0 ? $this->_paths : ['None']; + $message = __d( + 'cake_console', + "Current paths: %s\nWhat is the path you would like to extract?\n[Q]uit [D]one", + implode(', ', $currentPaths) + ); + $response = $this->in($message, null, $defaultPath); + if (strtoupper($response) === 'Q') { + $this->err(__d('cake_console', 'Extract Aborted')); + return $this->_stop(); + } else if (strtoupper($response) === 'D' && count($this->_paths)) { + $this->out(); + return; + } else if (strtoupper($response) === 'D') { + $this->err(__d('cake_console', 'No directories selected. Please choose a directory.')); + } else if (is_dir($response)) { + $this->_paths[] = $response; + $defaultPath = 'D'; + } else { + $this->err(__d('cake_console', 'The directory path you supplied was not found. Please try again.')); + } + $this->out(); + } + } + + /** + * Returns whether this execution is meant to extract string only from directories in folder represented by the + * APP constant, i.e. this task is extracting strings from same application. + * + * @return bool + */ + protected function _isExtractingApp() + { + return $this->_paths === [APP]; + } + + /** + * Checks whether or not a given path is usable for writing. + * + * @param string $path Path to folder + * @return bool true if it exists and is writable, false otherwise + */ + protected function _isPathUsable($path) + { + return is_dir($path) && is_writable($path); + } + + /** + * Search files that may contain translatable strings + * + * @return void + */ + protected function _searchFiles() + { + $pattern = false; + if (!empty($this->_exclude)) { + $exclude = []; + foreach ($this->_exclude as $e) { + if (DS !== '\\' && $e[0] !== DS) { + $e = DS . $e; + } + $exclude[] = preg_quote($e, '/'); + } + $pattern = '/' . implode('|', $exclude) . '/'; + } + foreach ($this->_paths as $i => $path) { + $this->_paths[$i] = realpath($path) . DS; + $Folder = new Folder($this->_paths[$i]); + $files = $Folder->findRecursive('.*\.(php|ctp|thtml|inc|tpl)', true); + if (!empty($pattern)) { + $files = preg_grep($pattern, $files, PREG_GREP_INVERT); + $files = array_values($files); + } + $this->_files = array_merge($this->_files, $files); + } + $this->_files = array_unique($this->_files); + } + + /** + * Extract text + * + * @return void + */ + protected function _extract() + { + $this->out(); + $this->out(); + $this->out(__d('cake_console', 'Extracting...')); + $this->hr(); + $this->out(__d('cake_console', 'Paths:')); + foreach ($this->_paths as $path) { + $this->out(' ' . $path); + } + $this->out(__d('cake_console', 'Output Directory: ') . $this->_output); + $this->hr(); + $this->_extractTokens(); + $this->_extractValidationMessages(); + $this->_buildFiles(); + $this->_writeFiles(); + $this->_paths = $this->_files = $this->_storage = []; + $this->_translations = $this->_tokens = []; + $this->_extractValidation = true; + $this->out(); + $this->out(__d('cake_console', 'Done.')); + } + + /** + * Extract tokens out of all files to be processed + * + * @return void + */ + protected function _extractTokens() + { + foreach ($this->_files as $file) { + $this->_file = $file; + $this->out(__d('cake_console', 'Processing %s...', $file), 1, Shell::VERBOSE); + + $code = file_get_contents($file); + $allTokens = token_get_all($code); + + $this->_tokens = []; + foreach ($allTokens as $token) { + if (!is_array($token) || ($token[0] !== T_WHITESPACE && $token[0] !== T_INLINE_HTML)) { + $this->_tokens[] = $token; + } + } + unset($allTokens); + $this->_parse('__', ['singular']); + $this->_parse('__n', ['singular', 'plural']); + $this->_parse('__d', ['domain', 'singular']); + $this->_parse('__c', ['singular', 'category']); + $this->_parse('__dc', ['domain', 'singular', 'category']); + $this->_parse('__dn', ['domain', 'singular', 'plural']); + $this->_parse('__dcn', ['domain', 'singular', 'plural', 'count', 'category']); + + $this->_parse('__x', ['context', 'singular']); + $this->_parse('__xn', ['context', 'singular', 'plural']); + $this->_parse('__dx', ['domain', 'context', 'singular']); + $this->_parse('__dxc', ['domain', 'context', 'singular', 'category']); + $this->_parse('__dxn', ['domain', 'context', 'singular', 'plural']); + $this->_parse('__dxcn', ['domain', 'context', 'singular', 'plural', 'count', 'category']); + $this->_parse('__xc', ['context', 'singular', 'category']); + + } + } + + /** + * Parse tokens + * + * @param string $functionName Function name that indicates translatable string (e.g: '__') + * @param array $map Array containing what variables it will find (e.g: category, domain, singular, plural) + * @return void + */ + protected function _parse($functionName, $map) + { + $count = 0; + $categories = ['LC_ALL', 'LC_COLLATE', 'LC_CTYPE', 'LC_MONETARY', 'LC_NUMERIC', 'LC_TIME', 'LC_MESSAGES']; + $tokenCount = count($this->_tokens); + + while (($tokenCount - $count) > 1) { + $countToken = $this->_tokens[$count]; + $firstParenthesis = $this->_tokens[$count + 1]; + if (!is_array($countToken)) { + $count++; + continue; + } + + list($type, $string, $line) = $countToken; + if (($type == T_STRING) && ($string === $functionName) && ($firstParenthesis === '(')) { + $position = $count; + $depth = 0; + + while (!$depth) { + if ($this->_tokens[$position] === '(') { + $depth++; + } else if ($this->_tokens[$position] === ')') { + $depth--; + } + $position++; + } + + $mapCount = count($map); + $strings = $this->_getStrings($position, $mapCount); + + if ($mapCount === count($strings)) { + extract(array_combine($map, $strings)); + $category = isset($category) ? $category : 6; + $category = (int)$category; + $categoryName = $categories[$category]; + + $domain = isset($domain) ? $domain : 'default'; + $details = [ + 'file' => $this->_file, + 'line' => $line, + ]; + if (isset($plural)) { + $details['msgid_plural'] = $plural; + } + if (isset($context)) { + $details['msgctxt'] = $context; + } + // Skip LC_TIME files as we use a special file format for them. + if ($categoryName !== 'LC_TIME') { + $this->_addTranslation($categoryName, $domain, $singular, $details); + } + } else if (!is_array($this->_tokens[$count - 1]) || $this->_tokens[$count - 1][0] != T_FUNCTION) { + $this->_markerError($this->_file, $line, $functionName, $count); + } + } + $count++; + } + } + + /** + * Get the strings from the position forward + * + * @param int &$position Actual position on tokens array + * @param int $target Number of strings to extract + * @return array Strings extracted + */ + protected function _getStrings(&$position, $target) + { + $strings = []; + $count = count($strings); + while ($count < $target && ($this->_tokens[$position] === ',' || $this->_tokens[$position][0] == T_CONSTANT_ENCAPSED_STRING || $this->_tokens[$position][0] == T_LNUMBER)) { + $count = count($strings); + if ($this->_tokens[$position][0] == T_CONSTANT_ENCAPSED_STRING && $this->_tokens[$position + 1] === '.') { + $string = ''; + while ($this->_tokens[$position][0] == T_CONSTANT_ENCAPSED_STRING || $this->_tokens[$position] === '.') { + if ($this->_tokens[$position][0] == T_CONSTANT_ENCAPSED_STRING) { + $string .= $this->_formatString($this->_tokens[$position][1]); + } + $position++; + } + $strings[] = $string; + } else if ($this->_tokens[$position][0] == T_CONSTANT_ENCAPSED_STRING) { + $strings[] = $this->_formatString($this->_tokens[$position][1]); + } else if ($this->_tokens[$position][0] == T_LNUMBER) { + $strings[] = $this->_tokens[$position][1]; + } + $position++; + } + return $strings; + } + + /** + * Format a string to be added as a translatable string + * + * @param string $string String to format + * @return string Formatted string + */ + protected function _formatString($string) + { + $quote = substr($string, 0, 1); + $string = substr($string, 1, -1); + if ($quote === '"') { + $string = stripcslashes($string); + } else { + $string = strtr($string, ["\\'" => "'", "\\\\" => "\\"]); + } + $string = str_replace("\r\n", "\n", $string); + return addcslashes($string, "\0..\37\\\""); + } + + /** + * Add a translation to the internal translations property + * + * Takes care of duplicate translations + * + * @param string $category The category + * @param string $domain The domain + * @param string $msgid The message string + * @param array $details The file and line references + * @return void + */ + protected function _addTranslation($category, $domain, $msgid, $details = []) + { + $context = ''; + if (isset($details['msgctxt'])) { + $context = $details['msgctxt']; + } + + if (empty($this->_translations[$category][$domain][$msgid][$context])) { + $this->_translations[$category][$domain][$msgid][$context] = [ + 'msgid_plural' => false, + ]; + } + + if (isset($details['msgid_plural'])) { + $this->_translations[$category][$domain][$msgid][$context]['msgid_plural'] = $details['msgid_plural']; + } + if (isset($details['file'])) { + $line = 0; + if (isset($details['line'])) { + $line = $details['line']; + } + $this->_translations[$category][$domain][$msgid][$context]['references'][$details['file']][] = $line; + } + } + + /** + * Indicate an invalid marker on a processed file + * + * @param string $file File where invalid marker resides + * @param int $line Line number + * @param string $marker Marker found + * @param int $count Count + * @return void + */ + protected function _markerError($file, $line, $marker, $count) + { + $this->err(__d('cake_console', "Invalid marker content in %s:%s\n* %s(", $file, $line, $marker)); + $count += 2; + $tokenCount = count($this->_tokens); + $parenthesis = 1; + + while ((($tokenCount - $count) > 0) && $parenthesis) { + if (is_array($this->_tokens[$count])) { + $this->err($this->_tokens[$count][1], false); + } else { + $this->err($this->_tokens[$count], false); + if ($this->_tokens[$count] === '(') { + $parenthesis++; + } + + if ($this->_tokens[$count] === ')') { + $parenthesis--; + } + } + $count++; + } + $this->err("\n", true); + } + + /** + * Looks for models in the application and extracts the validation messages + * to be added to the translation map + * + * @return void + */ + protected function _extractValidationMessages() + { + if (!$this->_extractValidation) { + return; + } + + $plugins = [null]; + if (empty($this->params['exclude-plugins'])) { + $plugins = array_merge($plugins, App::objects('plugin', null, false)); + } + foreach ($plugins as $plugin) { + $this->_extractPluginValidationMessages($plugin); + } + } + + /** + * Extract validation messages from application or plugin models + * + * @param string $plugin Plugin name or `null` to process application models + * @return void + */ + protected function _extractPluginValidationMessages($plugin = null) + { + App::uses('AppModel', 'Model'); + if (!empty($plugin)) { + if (!CakePlugin::loaded($plugin)) { + return; + } + App::uses($plugin . 'AppModel', $plugin . '.Model'); + $plugin = $plugin . '.'; + } + $models = App::objects($plugin . 'Model', null, false); + + foreach ($models as $model) { + App::uses($model, $plugin . 'Model'); + $reflection = new ReflectionClass($model); + if (!$reflection->isSubClassOf('Model')) { + continue; + } + $properties = $reflection->getDefaultProperties(); + $validate = $properties['validate']; + if (empty($validate)) { + continue; + } + + $file = $reflection->getFileName(); + $domain = $this->_validationDomain; + if (!empty($properties['validationDomain'])) { + $domain = $properties['validationDomain']; + } + foreach ($validate as $field => $rules) { + $this->_processValidationRules($field, $rules, $file, $domain); + } + } + } + + /** + * Process a validation rule for a field and looks for a message to be added + * to the translation map + * + * @param string $field the name of the field that is being processed + * @param array $rules the set of validation rules for the field + * @param string $file the file name where this validation rule was found + * @param string $domain default domain to bind the validations to + * @param string $category the translation category + * @return void + */ + protected function _processValidationRules($field, $rules, $file, $domain, $category = 'LC_MESSAGES') + { + if (!is_array($rules)) { + return; + } + + $dims = Hash::dimensions($rules); + if ($dims === 1 || ($dims === 2 && isset($rules['message']))) { + $rules = [$rules]; + } + + foreach ($rules as $rule => $validateProp) { + $msgid = null; + if (isset($validateProp['message'])) { + if (is_array($validateProp['message'])) { + $msgid = $validateProp['message'][0]; + } else { + $msgid = $validateProp['message']; + } + } else if (is_string($rule)) { + $msgid = $rule; + } + if ($msgid) { + $msgid = $this->_formatString(sprintf("'%s'", $msgid)); + $details = [ + 'file' => $file, + 'line' => 'validation for field ' . $field + ]; + $this->_addTranslation($category, $domain, $msgid, $details); + } + } + } + + /** + * Build the translate template file contents out of obtained strings + * + * @return void + */ + protected function _buildFiles() + { + $paths = $this->_paths; + $paths[] = realpath(APP) . DS; + + usort($paths, function ($a, $b) { + return strlen($b) - strlen($a); + }); + + foreach ($this->_translations as $category => $domains) { + foreach ($domains as $domain => $translations) { + foreach ($translations as $msgid => $contexts) { + foreach ($contexts as $context => $details) { + $plural = $details['msgid_plural']; + $header = ''; + if (empty($this->params['no-location'])) { + $files = $details['references']; + $occurrences = []; + foreach ($files as $file => $lines) { + $lines = array_unique($lines); + $occurrences[] = $file . ':' . implode(';', $lines); + } + $occurrences = implode("\n#: ", $occurrences); + $header = '#: ' . str_replace(DS, '/', str_replace($paths, '', $occurrences)) . "\n"; + } + + $sentence = ''; + if ($context) { + $sentence .= "msgctxt \"{$context}\"\n"; + } + if ($plural === false) { + $sentence .= "msgid \"{$msgid}\"\n"; + $sentence .= "msgstr \"\"\n\n"; + } else { + $sentence .= "msgid \"{$msgid}\"\n"; + $sentence .= "msgid_plural \"{$plural}\"\n"; + $sentence .= "msgstr[0] \"\"\n"; + $sentence .= "msgstr[1] \"\"\n\n"; + } + + $this->_store($category, $domain, $header, $sentence); + if (($category !== 'LC_MESSAGES' || $domain !== 'default') && $this->_merge) { + $this->_store('LC_MESSAGES', 'default', $header, $sentence); + } + } + } + } + } + } + + /** + * Prepare a file to be stored + * + * @param string $category The category + * @param string $domain The domain + * @param string $header The header content. + * @param string $sentence The sentence to store. + * @return void + */ + protected function _store($category, $domain, $header, $sentence) + { + if (!isset($this->_storage[$category])) { + $this->_storage[$category] = []; + } + if (!isset($this->_storage[$category][$domain])) { + $this->_storage[$category][$domain] = []; + } + if (!isset($this->_storage[$category][$domain][$sentence])) { + $this->_storage[$category][$domain][$sentence] = $header; + } else { + $this->_storage[$category][$domain][$sentence] .= $header; + } + } + + /** + * Write the files that need to be stored + * + * @return void + */ + protected function _writeFiles() + { + $overwriteAll = false; + if (!empty($this->params['overwrite'])) { + $overwriteAll = true; + } + foreach ($this->_storage as $category => $domains) { + foreach ($domains as $domain => $sentences) { + $output = $this->_writeHeader(); + foreach ($sentences as $sentence => $header) { + $output .= $header . $sentence; + } + + $filename = $domain . '.pot'; + if ($category === 'LC_MESSAGES') { + $File = new File($this->_output . $filename); + } else { + new Folder($this->_output . $category, true); + $File = new File($this->_output . $category . DS . $filename); + } + $response = ''; + while ($overwriteAll === false && $File->exists() && strtoupper($response) !== 'Y') { + $this->out(); + $response = $this->in( + __d('cake_console', 'Error: %s already exists in this location. Overwrite? [Y]es, [N]o, [A]ll', $filename), + ['y', 'n', 'a'], + 'y' + ); + if (strtoupper($response) === 'N') { + $response = ''; + while (!$response) { + $response = $this->in(__d('cake_console', "What would you like to name this file?"), null, 'new_' . $filename); + $File = new File($this->_output . $response); + $filename = $response; + } + } else if (strtoupper($response) === 'A') { + $overwriteAll = true; + } + } + $File->write($output); + $File->close(); + } + } + } + + /** + * Build the translation template header + * + * @return string Translation template header + */ + protected function _writeHeader() + { + $output = "# LANGUAGE translation of CakePHP Application\n"; + $output .= "# Copyright YEAR NAME \n"; + $output .= "#\n"; + $output .= "#, fuzzy\n"; + $output .= "msgid \"\"\n"; + $output .= "msgstr \"\"\n"; + $output .= "\"Project-Id-Version: PROJECT VERSION\\n\"\n"; + $output .= "\"PO-Revision-Date: YYYY-mm-DD HH:MM+ZZZZ\\n\"\n"; + $output .= "\"Last-Translator: NAME \\n\"\n"; + $output .= "\"Language-Team: LANGUAGE \\n\"\n"; + $output .= "\"MIME-Version: 1.0\\n\"\n"; + $output .= "\"Content-Type: text/plain; charset=utf-8\\n\"\n"; + $output .= "\"Content-Transfer-Encoding: 8bit\\n\"\n"; + $output .= "\"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\\n\"\n\n"; + return $output; + } + + /** + * Gets the option parser instance and configures it. + * + * @return ConsoleOptionParser + */ + public function getOptionParser() + { + $parser = parent::getOptionParser(); + + $parser->description( + __d('cake_console', 'CakePHP Language String Extraction:') + )->addOption('app', [ + 'help' => __d('cake_console', 'Directory where your application is located.') + ])->addOption('paths', [ + 'help' => __d('cake_console', 'Comma separated list of paths.') + ])->addOption('merge', [ + 'help' => __d('cake_console', 'Merge all domain and category strings into the default.po file.'), + 'choices' => ['yes', 'no'] + ])->addOption('no-location', [ + 'boolean' => true, + 'default' => false, + 'help' => __d('cake_console', 'Do not write lines with locations'), + ])->addOption('output', [ + 'help' => __d('cake_console', 'Full path to output directory.') + ])->addOption('files', [ + 'help' => __d('cake_console', 'Comma separated list of files.') + ])->addOption('exclude-plugins', [ + 'boolean' => true, + 'default' => true, + 'help' => __d('cake_console', 'Ignores all files in plugins if this command is run inside from the same app directory.') + ])->addOption('plugin', [ + 'help' => __d('cake_console', 'Extracts tokens only from the plugin specified and puts the result in the plugin\'s Locale directory.') + ])->addOption('ignore-model-validation', [ + 'boolean' => true, + 'default' => false, + 'help' => __d('cake_console', 'Ignores validation messages in the $validate property.' . + ' If this flag is not set and the command is run from the same app directory,' . + ' all messages in model validation rules will be extracted as tokens.' + ) + ])->addOption('validation-domain', [ + 'help' => __d('cake_console', 'If set to a value, the localization domain to be used for model validation messages.') + ])->addOption('exclude', [ + 'help' => __d('cake_console', 'Comma separated list of directories to exclude.' . + ' Any path containing a path segment with the provided values will be skipped. E.g. test,vendors' + ) + ])->addOption('overwrite', [ + 'boolean' => true, + 'default' => false, + 'help' => __d('cake_console', 'Always overwrite existing .pot files.') + ])->addOption('extract-core', [ + 'help' => __d('cake_console', 'Extract messages from the CakePHP core libs.'), + 'choices' => ['yes', 'no'] + ]); + + return $parser; + } } diff --git a/lib/Cake/Console/Command/Task/FixtureTask.php b/lib/Cake/Console/Command/Task/FixtureTask.php index 7b2ee52e..a2ab4fdc 100755 --- a/lib/Cake/Console/Command/Task/FixtureTask.php +++ b/lib/Cake/Console/Command/Task/FixtureTask.php @@ -24,444 +24,458 @@ * * @package Cake.Console.Command.Task */ -class FixtureTask extends BakeTask { - -/** - * Tasks to be loaded by this Task - * - * @var array - */ - public $tasks = array('DbConfig', 'Model', 'Template'); - -/** - * path to fixtures directory - * - * @var string - */ - public $path = null; - -/** - * Schema instance - * - * @var CakeSchema - */ - protected $_Schema = null; - -/** - * Override initialize - * - * @param ConsoleOutput $stdout A ConsoleOutput object for stdout. - * @param ConsoleOutput $stderr A ConsoleOutput object for stderr. - * @param ConsoleInput $stdin A ConsoleInput object for stdin. - */ - public function __construct($stdout = null, $stderr = null, $stdin = null) { - parent::__construct($stdout, $stderr, $stdin); - $this->path = APP . 'Test' . DS . 'Fixture' . DS; - } - -/** - * Gets the option parser instance and configures it. - * - * @return ConsoleOptionParser - */ - public function getOptionParser() { - $parser = parent::getOptionParser(); - - $parser->description( - __d('cake_console', 'Generate fixtures for use with the test suite. You can use `bake fixture all` to bake all fixtures.') - )->addArgument('name', array( - 'help' => __d('cake_console', 'Name of the fixture to bake. Can use Plugin.name to bake plugin fixtures.') - ))->addOption('count', array( - 'help' => __d('cake_console', 'When using generated data, the number of records to include in the fixture(s).'), - 'short' => 'n', - 'default' => 1 - ))->addOption('connection', array( - 'help' => __d('cake_console', 'Which database configuration to use for baking.'), - 'short' => 'c', - 'default' => 'default' - ))->addOption('plugin', array( - 'help' => __d('cake_console', 'CamelCased name of the plugin to bake fixtures for.'), - 'short' => 'p' - ))->addOption('schema', array( - 'help' => __d('cake_console', 'Importing schema for fixtures rather than hardcoding it.'), - 'short' => 's', - 'boolean' => true - ))->addOption('theme', array( - 'short' => 't', - 'help' => __d('cake_console', 'Theme to use when baking code.') - ))->addOption('force', array( - 'short' => 'f', - 'help' => __d('cake_console', 'Force overwriting existing files without prompting.') - ))->addOption('records', array( - 'help' => __d('cake_console', 'Used with --count and /all commands to pull [n] records from the live tables, ' . - 'where [n] is either --count or the default of 10.'), - 'short' => 'r', - 'boolean' => true - ))->epilog( - __d('cake_console', 'Omitting all arguments and options will enter into an interactive mode.') - ); - - return $parser; - } - -/** - * Execution method always used for tasks - * Handles dispatching to interactive, named, or all processes. - * - * @return void - */ - public function execute() { - parent::execute(); - if (empty($this->args)) { - $this->_interactive(); - } - - if (isset($this->args[0])) { - $this->interactive = false; - if (!isset($this->connection)) { - $this->connection = 'default'; - } - if (strtolower($this->args[0]) === 'all') { - return $this->all(); - } - $model = $this->_modelName($this->args[0]); - $importOptions = $this->importOptions($model); - $this->bake($model, false, $importOptions); - } - } - -/** - * Bake All the Fixtures at once. Will only bake fixtures for models that exist. - * - * @return void - */ - public function all() { - $this->interactive = false; - $this->Model->interactive = false; - $tables = $this->Model->listAll($this->connection, false); - - foreach ($tables as $table) { - $model = $this->_modelName($table); - $importOptions = array(); - if (!empty($this->params['schema'])) { - $importOptions['schema'] = $model; - } - $this->bake($model, false, $importOptions); - } - } - -/** - * Interactive baking function - * - * @return void - */ - protected function _interactive() { - $this->DbConfig->interactive = $this->Model->interactive = $this->interactive = true; - $this->hr(); - $this->out(__d('cake_console', "Bake Fixture\nPath: %s", $this->getPath())); - $this->hr(); - - if (!isset($this->connection)) { - $this->connection = $this->DbConfig->getConfig(); - } - $modelName = $this->Model->getName($this->connection); - $useTable = $this->Model->getTable($modelName, $this->connection); - $importOptions = $this->importOptions($modelName); - $this->bake($modelName, $useTable, $importOptions); - } - -/** - * Interacts with the User to setup an array of import options. For a fixture. - * - * @param string $modelName Name of model you are dealing with. - * @return array Array of import options. - */ - public function importOptions($modelName) { - $options = array(); - $plugin = ''; - if (isset($this->params['plugin'])) { - $plugin = $this->params['plugin'] . '.'; - } - - if (!empty($this->params['schema'])) { - $options['schema'] = $plugin . $modelName; - } elseif ($this->interactive) { - $doSchema = $this->in(__d('cake_console', 'Would you like to import schema for this fixture?'), array('y', 'n'), 'n'); - if ($doSchema === 'y') { - $options['schema'] = $modelName; - } - } - - if (!empty($this->params['records'])) { - $options['fromTable'] = true; - } elseif ($this->interactive) { - $doRecords = $this->in(__d('cake_console', 'Would you like to use record importing for this fixture?'), array('y', 'n'), 'n'); - if ($doRecords === 'y') { - $options['records'] = true; - } - } - if (!isset($options['records']) && $this->interactive) { - $prompt = __d('cake_console', "Would you like to build this fixture with data from %s's table?", $modelName); - $fromTable = $this->in($prompt, array('y', 'n'), 'n'); - if (strtolower($fromTable) === 'y') { - $options['fromTable'] = true; - } - } - return $options; - } - -/** - * Assembles and writes a Fixture file - * - * @param string $model Name of model to bake. - * @param string $useTable Name of table to use. - * @param array $importOptions Options for public $import - * @return string|null Baked fixture content, otherwise null. - */ - public function bake($model, $useTable = false, $importOptions = array()) { - App::uses('CakeSchema', 'Model'); - $table = $schema = $records = $import = $modelImport = null; - $importBits = array(); - - if (!$useTable) { - $useTable = Inflector::tableize($model); - } elseif ($useTable != Inflector::tableize($model)) { - $table = $useTable; - } - - if (!empty($importOptions)) { - if (isset($importOptions['schema'])) { - $modelImport = true; - $importBits[] = "'model' => '{$importOptions['schema']}'"; - } - if (isset($importOptions['records'])) { - $importBits[] = "'records' => true"; - } - if ($this->connection !== 'default') { - $importBits[] .= "'connection' => '{$this->connection}'"; - } - if (!empty($importBits)) { - $import = sprintf("array(%s)", implode(', ', $importBits)); - } - } - - $this->_Schema = new CakeSchema(); - $data = $this->_Schema->read(array('models' => false, 'connection' => $this->connection)); - if (!isset($data['tables'][$useTable])) { - $this->err("Warning: Could not find the '${useTable}' table for ${model}."); - return null; - } - - $tableInfo = $data['tables'][$useTable]; - if ($modelImport === null) { - $schema = $this->_generateSchema($tableInfo); - } - - if (empty($importOptions['records']) && !isset($importOptions['fromTable'])) { - $recordCount = 1; - if (isset($this->params['count'])) { - $recordCount = $this->params['count']; - } - $records = $this->_makeRecordString($this->_generateRecords($tableInfo, $recordCount)); - } - if (!empty($this->params['records']) || isset($importOptions['fromTable'])) { - $records = $this->_makeRecordString($this->_getRecordsFromTable($model, $useTable)); - } - $out = $this->generateFixtureFile($model, compact('records', 'table', 'schema', 'import')); - return $out; - } - -/** - * Generate the fixture file, and write to disk - * - * @param string $model name of the model being generated - * @param string $otherVars Contents of the fixture file. - * @return string Content saved into fixture file. - */ - public function generateFixtureFile($model, $otherVars) { - $defaults = array('table' => null, 'schema' => null, 'records' => null, 'import' => null, 'fields' => null); - $vars = array_merge($defaults, $otherVars); - - $path = $this->getPath(); - $filename = Inflector::camelize($model) . 'Fixture.php'; - - $this->Template->set('model', $model); - $this->Template->set($vars); - $content = $this->Template->generate('classes', 'fixture'); - - $this->out("\n" . __d('cake_console', 'Baking test fixture for %s...', $model), 1, Shell::QUIET); - $this->createFile($path . $filename, $content); - return $content; - } - -/** - * Get the path to the fixtures. - * - * @return string Path for the fixtures - */ - public function getPath() { - $path = $this->path; - if (isset($this->plugin)) { - $path = $this->_pluginPath($this->plugin) . 'Test' . DS . 'Fixture' . DS; - } - return $path; - } - -/** - * Generates a string representation of a schema. - * - * @param array $tableInfo Table schema array - * @return string fields definitions - */ - protected function _generateSchema($tableInfo) { - $schema = trim($this->_Schema->generateTable('f', $tableInfo), "\n"); - return substr($schema, 13, -1); - } - -/** - * Generate String representation of Records - * - * @param array $tableInfo Table schema array - * @param int $recordCount The number of records to generate. - * @return array Array of records to use in the fixture. - */ - protected function _generateRecords($tableInfo, $recordCount = 1) { - $records = array(); - for ($i = 0; $i < $recordCount; $i++) { - $record = array(); - foreach ($tableInfo as $field => $fieldInfo) { - if (empty($fieldInfo['type'])) { - continue; - } - $insert = ''; - switch ($fieldInfo['type']) { - case 'tinyinteger': - case 'smallinteger': - case 'biginteger': - case 'integer': - case 'float': - $insert = $i + 1; - break; - case 'string': - case 'binary': - $isPrimaryUuid = ( - isset($fieldInfo['key']) && strtolower($fieldInfo['key']) === 'primary' && - isset($fieldInfo['length']) && $fieldInfo['length'] == 36 - ); - if ($isPrimaryUuid) { - $insert = CakeText::uuid(); - } else { - $insert = "Lorem ipsum dolor sit amet"; - if (!empty($fieldInfo['length'])) { - $insert = substr($insert, 0, (int)$fieldInfo['length'] - 2); - } - } - break; - case 'timestamp': - $insert = time(); - break; - case 'datetime': - $insert = date('Y-m-d H:i:s'); - break; - case 'date': - $insert = date('Y-m-d'); - break; - case 'time': - $insert = date('H:i:s'); - break; - case 'boolean': - $insert = 1; - break; - case 'text': - $insert = "Lorem ipsum dolor sit amet, aliquet feugiat."; - $insert .= " Convallis morbi fringilla gravida,"; - $insert .= " phasellus feugiat dapibus velit nunc, pulvinar eget sollicitudin"; - $insert .= " venenatis cum nullam, vivamus ut a sed, mollitia lectus. Nulla"; - $insert .= " vestibulum massa neque ut et, id hendrerit sit,"; - $insert .= " feugiat in taciti enim proin nibh, tempor dignissim, rhoncus"; - $insert .= " duis vestibulum nunc mattis convallis."; - break; - } - $record[$field] = $insert; - } - $records[] = $record; - } - return $records; - } - -/** - * Convert a $records array into a string. - * - * @param array $records Array of records to be converted to string - * @return string A string value of the $records array. - */ - protected function _makeRecordString($records) { - $out = "array(\n"; - foreach ($records as $record) { - $values = array(); - foreach ($record as $field => $value) { - $val = var_export($value, true); - if ($val === 'NULL') { - $val = 'null'; - } - $values[] = "\t\t\t'$field' => $val"; - } - $out .= "\t\tarray(\n"; - $out .= implode(",\n", $values); - $out .= "\n\t\t),\n"; - } - $out .= "\t)"; - return $out; - } - -/** - * Interact with the user to get a custom SQL condition and use that to extract data - * to build a fixture. - * - * @param string $modelName name of the model to take records from. - * @param string $useTable Name of table to use. - * @return array Array of records. - */ - protected function _getRecordsFromTable($modelName, $useTable = null) { - $modelObject = new Model(array('name' => $modelName, 'table' => $useTable, 'ds' => $this->connection)); - if ($this->interactive) { - $condition = null; - $prompt = __d('cake_console', "Please provide a SQL fragment to use as conditions\nExample: WHERE 1=1"); - while (!$condition) { - $condition = $this->in($prompt, null, 'WHERE 1=1'); - } - - $recordsFound = $modelObject->find('count', array( - 'conditions' => $condition, - 'recursive' => -1, - )); - - $prompt = __d('cake_console', "How many records do you want to import?"); - $recordCount = $this->in($prompt, null, ($recordsFound < 10 ) ? $recordsFound : 10); - } else { - $condition = 'WHERE 1=1'; - $recordCount = (isset($this->params['count']) ? $this->params['count'] : 10); - } - - $records = $modelObject->find('all', array( - 'conditions' => $condition, - 'recursive' => -1, - 'limit' => $recordCount - )); - - $schema = $modelObject->schema(true); - $out = array(); - foreach ($records as $record) { - $row = array(); - foreach ($record[$modelObject->alias] as $field => $value) { - if ($schema[$field]['type'] === 'boolean') { - $value = (int)(bool)$value; - } - $row[$field] = $value; - } - $out[] = $row; - } - return $out; - } +class FixtureTask extends BakeTask +{ + + /** + * Tasks to be loaded by this Task + * + * @var array + */ + public $tasks = ['DbConfig', 'Model', 'Template']; + + /** + * path to fixtures directory + * + * @var string + */ + public $path = null; + + /** + * Schema instance + * + * @var CakeSchema + */ + protected $_Schema = null; + + /** + * Override initialize + * + * @param ConsoleOutput $stdout A ConsoleOutput object for stdout. + * @param ConsoleOutput $stderr A ConsoleOutput object for stderr. + * @param ConsoleInput $stdin A ConsoleInput object for stdin. + */ + public function __construct($stdout = null, $stderr = null, $stdin = null) + { + parent::__construct($stdout, $stderr, $stdin); + $this->path = APP . 'Test' . DS . 'Fixture' . DS; + } + + /** + * Gets the option parser instance and configures it. + * + * @return ConsoleOptionParser + */ + public function getOptionParser() + { + $parser = parent::getOptionParser(); + + $parser->description( + __d('cake_console', 'Generate fixtures for use with the test suite. You can use `bake fixture all` to bake all fixtures.') + )->addArgument('name', [ + 'help' => __d('cake_console', 'Name of the fixture to bake. Can use Plugin.name to bake plugin fixtures.') + ])->addOption('count', [ + 'help' => __d('cake_console', 'When using generated data, the number of records to include in the fixture(s).'), + 'short' => 'n', + 'default' => 1 + ])->addOption('connection', [ + 'help' => __d('cake_console', 'Which database configuration to use for baking.'), + 'short' => 'c', + 'default' => 'default' + ])->addOption('plugin', [ + 'help' => __d('cake_console', 'CamelCased name of the plugin to bake fixtures for.'), + 'short' => 'p' + ])->addOption('schema', [ + 'help' => __d('cake_console', 'Importing schema for fixtures rather than hardcoding it.'), + 'short' => 's', + 'boolean' => true + ])->addOption('theme', [ + 'short' => 't', + 'help' => __d('cake_console', 'Theme to use when baking code.') + ])->addOption('force', [ + 'short' => 'f', + 'help' => __d('cake_console', 'Force overwriting existing files without prompting.') + ])->addOption('records', [ + 'help' => __d('cake_console', 'Used with --count and /all commands to pull [n] records from the live tables, ' . + 'where [n] is either --count or the default of 10.'), + 'short' => 'r', + 'boolean' => true + ])->epilog( + __d('cake_console', 'Omitting all arguments and options will enter into an interactive mode.') + ); + + return $parser; + } + + /** + * Execution method always used for tasks + * Handles dispatching to interactive, named, or all processes. + * + * @return void + */ + public function execute() + { + parent::execute(); + if (empty($this->args)) { + $this->_interactive(); + } + + if (isset($this->args[0])) { + $this->interactive = false; + if (!isset($this->connection)) { + $this->connection = 'default'; + } + if (strtolower($this->args[0]) === 'all') { + return $this->all(); + } + $model = $this->_modelName($this->args[0]); + $importOptions = $this->importOptions($model); + $this->bake($model, false, $importOptions); + } + } + + /** + * Interactive baking function + * + * @return void + */ + protected function _interactive() + { + $this->DbConfig->interactive = $this->Model->interactive = $this->interactive = true; + $this->hr(); + $this->out(__d('cake_console', "Bake Fixture\nPath: %s", $this->getPath())); + $this->hr(); + + if (!isset($this->connection)) { + $this->connection = $this->DbConfig->getConfig(); + } + $modelName = $this->Model->getName($this->connection); + $useTable = $this->Model->getTable($modelName, $this->connection); + $importOptions = $this->importOptions($modelName); + $this->bake($modelName, $useTable, $importOptions); + } + + /** + * Get the path to the fixtures. + * + * @return string Path for the fixtures + */ + public function getPath() + { + $path = $this->path; + if (isset($this->plugin)) { + $path = $this->_pluginPath($this->plugin) . 'Test' . DS . 'Fixture' . DS; + } + return $path; + } + + /** + * Interacts with the User to setup an array of import options. For a fixture. + * + * @param string $modelName Name of model you are dealing with. + * @return array Array of import options. + */ + public function importOptions($modelName) + { + $options = []; + $plugin = ''; + if (isset($this->params['plugin'])) { + $plugin = $this->params['plugin'] . '.'; + } + + if (!empty($this->params['schema'])) { + $options['schema'] = $plugin . $modelName; + } else if ($this->interactive) { + $doSchema = $this->in(__d('cake_console', 'Would you like to import schema for this fixture?'), ['y', 'n'], 'n'); + if ($doSchema === 'y') { + $options['schema'] = $modelName; + } + } + + if (!empty($this->params['records'])) { + $options['fromTable'] = true; + } else if ($this->interactive) { + $doRecords = $this->in(__d('cake_console', 'Would you like to use record importing for this fixture?'), ['y', 'n'], 'n'); + if ($doRecords === 'y') { + $options['records'] = true; + } + } + if (!isset($options['records']) && $this->interactive) { + $prompt = __d('cake_console', "Would you like to build this fixture with data from %s's table?", $modelName); + $fromTable = $this->in($prompt, ['y', 'n'], 'n'); + if (strtolower($fromTable) === 'y') { + $options['fromTable'] = true; + } + } + return $options; + } + + /** + * Assembles and writes a Fixture file + * + * @param string $model Name of model to bake. + * @param string $useTable Name of table to use. + * @param array $importOptions Options for public $import + * @return string|null Baked fixture content, otherwise null. + */ + public function bake($model, $useTable = false, $importOptions = []) + { + App::uses('CakeSchema', 'Model'); + $table = $schema = $records = $import = $modelImport = null; + $importBits = []; + + if (!$useTable) { + $useTable = Inflector::tableize($model); + } else if ($useTable != Inflector::tableize($model)) { + $table = $useTable; + } + + if (!empty($importOptions)) { + if (isset($importOptions['schema'])) { + $modelImport = true; + $importBits[] = "'model' => '{$importOptions['schema']}'"; + } + if (isset($importOptions['records'])) { + $importBits[] = "'records' => true"; + } + if ($this->connection !== 'default') { + $importBits[] .= "'connection' => '{$this->connection}'"; + } + if (!empty($importBits)) { + $import = sprintf("array(%s)", implode(', ', $importBits)); + } + } + + $this->_Schema = new CakeSchema(); + $data = $this->_Schema->read(['models' => false, 'connection' => $this->connection]); + if (!isset($data['tables'][$useTable])) { + $this->err("Warning: Could not find the '${useTable}' table for ${model}."); + return null; + } + + $tableInfo = $data['tables'][$useTable]; + if ($modelImport === null) { + $schema = $this->_generateSchema($tableInfo); + } + + if (empty($importOptions['records']) && !isset($importOptions['fromTable'])) { + $recordCount = 1; + if (isset($this->params['count'])) { + $recordCount = $this->params['count']; + } + $records = $this->_makeRecordString($this->_generateRecords($tableInfo, $recordCount)); + } + if (!empty($this->params['records']) || isset($importOptions['fromTable'])) { + $records = $this->_makeRecordString($this->_getRecordsFromTable($model, $useTable)); + } + $out = $this->generateFixtureFile($model, compact('records', 'table', 'schema', 'import')); + return $out; + } + + /** + * Generates a string representation of a schema. + * + * @param array $tableInfo Table schema array + * @return string fields definitions + */ + protected function _generateSchema($tableInfo) + { + $schema = trim($this->_Schema->generateTable('f', $tableInfo), "\n"); + return substr($schema, 13, -1); + } + + /** + * Convert a $records array into a string. + * + * @param array $records Array of records to be converted to string + * @return string A string value of the $records array. + */ + protected function _makeRecordString($records) + { + $out = "array(\n"; + foreach ($records as $record) { + $values = []; + foreach ($record as $field => $value) { + $val = var_export($value, true); + if ($val === 'NULL') { + $val = 'null'; + } + $values[] = "\t\t\t'$field' => $val"; + } + $out .= "\t\tarray(\n"; + $out .= implode(",\n", $values); + $out .= "\n\t\t),\n"; + } + $out .= "\t)"; + return $out; + } + + /** + * Generate String representation of Records + * + * @param array $tableInfo Table schema array + * @param int $recordCount The number of records to generate. + * @return array Array of records to use in the fixture. + */ + protected function _generateRecords($tableInfo, $recordCount = 1) + { + $records = []; + for ($i = 0; $i < $recordCount; $i++) { + $record = []; + foreach ($tableInfo as $field => $fieldInfo) { + if (empty($fieldInfo['type'])) { + continue; + } + $insert = ''; + switch ($fieldInfo['type']) { + case 'tinyinteger': + case 'smallinteger': + case 'biginteger': + case 'integer': + case 'float': + $insert = $i + 1; + break; + case 'string': + case 'binary': + $isPrimaryUuid = ( + isset($fieldInfo['key']) && strtolower($fieldInfo['key']) === 'primary' && + isset($fieldInfo['length']) && $fieldInfo['length'] == 36 + ); + if ($isPrimaryUuid) { + $insert = CakeText::uuid(); + } else { + $insert = "Lorem ipsum dolor sit amet"; + if (!empty($fieldInfo['length'])) { + $insert = substr($insert, 0, (int)$fieldInfo['length'] - 2); + } + } + break; + case 'timestamp': + $insert = time(); + break; + case 'datetime': + $insert = date('Y-m-d H:i:s'); + break; + case 'date': + $insert = date('Y-m-d'); + break; + case 'time': + $insert = date('H:i:s'); + break; + case 'boolean': + $insert = 1; + break; + case 'text': + $insert = "Lorem ipsum dolor sit amet, aliquet feugiat."; + $insert .= " Convallis morbi fringilla gravida,"; + $insert .= " phasellus feugiat dapibus velit nunc, pulvinar eget sollicitudin"; + $insert .= " venenatis cum nullam, vivamus ut a sed, mollitia lectus. Nulla"; + $insert .= " vestibulum massa neque ut et, id hendrerit sit,"; + $insert .= " feugiat in taciti enim proin nibh, tempor dignissim, rhoncus"; + $insert .= " duis vestibulum nunc mattis convallis."; + break; + } + $record[$field] = $insert; + } + $records[] = $record; + } + return $records; + } + + /** + * Interact with the user to get a custom SQL condition and use that to extract data + * to build a fixture. + * + * @param string $modelName name of the model to take records from. + * @param string $useTable Name of table to use. + * @return array Array of records. + */ + protected function _getRecordsFromTable($modelName, $useTable = null) + { + $modelObject = new Model(['name' => $modelName, 'table' => $useTable, 'ds' => $this->connection]); + if ($this->interactive) { + $condition = null; + $prompt = __d('cake_console', "Please provide a SQL fragment to use as conditions\nExample: WHERE 1=1"); + while (!$condition) { + $condition = $this->in($prompt, null, 'WHERE 1=1'); + } + + $recordsFound = $modelObject->find('count', [ + 'conditions' => $condition, + 'recursive' => -1, + ]); + + $prompt = __d('cake_console', "How many records do you want to import?"); + $recordCount = $this->in($prompt, null, ($recordsFound < 10) ? $recordsFound : 10); + } else { + $condition = 'WHERE 1=1'; + $recordCount = (isset($this->params['count']) ? $this->params['count'] : 10); + } + + $records = $modelObject->find('all', [ + 'conditions' => $condition, + 'recursive' => -1, + 'limit' => $recordCount + ]); + + $schema = $modelObject->schema(true); + $out = []; + foreach ($records as $record) { + $row = []; + foreach ($record[$modelObject->alias] as $field => $value) { + if ($schema[$field]['type'] === 'boolean') { + $value = (int)(bool)$value; + } + $row[$field] = $value; + } + $out[] = $row; + } + return $out; + } + + /** + * Generate the fixture file, and write to disk + * + * @param string $model name of the model being generated + * @param string $otherVars Contents of the fixture file. + * @return string Content saved into fixture file. + */ + public function generateFixtureFile($model, $otherVars) + { + $defaults = ['table' => null, 'schema' => null, 'records' => null, 'import' => null, 'fields' => null]; + $vars = array_merge($defaults, $otherVars); + + $path = $this->getPath(); + $filename = Inflector::camelize($model) . 'Fixture.php'; + + $this->Template->set('model', $model); + $this->Template->set($vars); + $content = $this->Template->generate('classes', 'fixture'); + + $this->out("\n" . __d('cake_console', 'Baking test fixture for %s...', $model), 1, Shell::QUIET); + $this->createFile($path . $filename, $content); + return $content; + } + + /** + * Bake All the Fixtures at once. Will only bake fixtures for models that exist. + * + * @return void + */ + public function all() + { + $this->interactive = false; + $this->Model->interactive = false; + $tables = $this->Model->listAll($this->connection, false); + + foreach ($tables as $table) { + $model = $this->_modelName($table); + $importOptions = []; + if (!empty($this->params['schema'])) { + $importOptions['schema'] = $model; + } + $this->bake($model, false, $importOptions); + } + } } diff --git a/lib/Cake/Console/Command/Task/ModelTask.php b/lib/Cake/Console/Command/Task/ModelTask.php index 88be255c..aa5c7d1f 100755 --- a/lib/Cake/Console/Command/Task/ModelTask.php +++ b/lib/Cake/Console/Command/Task/ModelTask.php @@ -24,1034 +24,1063 @@ /** * Task class for creating and updating model files. * - * @package Cake.Console.Command.Task + * @package Cake.Console.Command.Task */ -class ModelTask extends BakeTask { - -/** - * path to Model directory - * - * @var string - */ - public $path = null; - -/** - * tasks - * - * @var array - */ - public $tasks = array('DbConfig', 'Fixture', 'Test', 'Template'); - -/** - * Tables to skip when running all() - * - * @var array - */ - public $skipTables = array('i18n'); - -/** - * Holds tables found on connection. - * - * @var array - */ - protected $_tables = array(); - -/** - * Holds the model names - * - * @var array - */ - protected $_modelNames = array(); - -/** - * Holds validation method map. - * - * @var array - */ - protected $_validations = array(); - -/** - * Override initialize - * - * @return void - */ - public function initialize() { - $this->path = current(App::path('Model')); - } - -/** - * Execution method always used for tasks - * - * @return void - */ - public function execute() { - parent::execute(); - - if (empty($this->args)) { - $this->_interactive(); - } - - if (!empty($this->args[0])) { - $this->interactive = false; - if (!isset($this->connection)) { - $this->connection = 'default'; - } - if (strtolower($this->args[0]) === 'all') { - return $this->all(); - } - $model = $this->_modelName($this->args[0]); - $this->listAll($this->connection); - $useTable = $this->getTable($model); - $object = $this->_getModelObject($model, $useTable); - if ($this->bake($object, false)) { - if ($this->_checkUnitTest()) { - $this->bakeFixture($model, $useTable); - $this->bakeTest($model); - } - } - } - } - -/** - * Bake all models at once. - * - * @return void - */ - public function all() { - $this->listAll($this->connection, false); - $unitTestExists = $this->_checkUnitTest(); - foreach ($this->_tables as $table) { - if (in_array($table, $this->skipTables)) { - continue; - } - $modelClass = Inflector::classify($table); - $this->out(__d('cake_console', 'Baking %s', $modelClass)); - $object = $this->_getModelObject($modelClass, $table); - if ($this->bake($object, false) && $unitTestExists) { - $this->bakeFixture($modelClass, $table); - $this->bakeTest($modelClass); - } - } - } - -/** - * Get a model object for a class name. - * - * @param string $className Name of class you want model to be. - * @param string $table Table name - * @return Model Model instance - */ - protected function _getModelObject($className, $table = null) { - if (!$table) { - $table = Inflector::tableize($className); - } - $object = new Model(array('name' => $className, 'table' => $table, 'ds' => $this->connection)); - $fields = $object->schema(true); - foreach ($fields as $name => $field) { - if (isset($field['key']) && $field['key'] === 'primary') { - $object->primaryKey = $name; - break; - } - } - return $object; - } - -/** - * Generate a key value list of options and a prompt. - * - * @param array $options Array of options to use for the selections. indexes must start at 0 - * @param string $prompt Prompt to use for options list. - * @param int $default The default option for the given prompt. - * @return int Result of user choice. - */ - public function inOptions($options, $prompt = null, $default = null) { - $valid = false; - $max = count($options); - while (!$valid) { - $len = strlen(count($options) + 1); - foreach ($options as $i => $option) { - $this->out(sprintf("%${len}d. %s", $i + 1, $option)); - } - if (empty($prompt)) { - $prompt = __d('cake_console', 'Make a selection from the choices above'); - } - $choice = $this->in($prompt, null, $default); - if ((int)$choice > 0 && (int)$choice <= $max) { - $valid = true; - } - } - return $choice - 1; - } - -/** - * Handles interactive baking - * - * @return bool - */ - protected function _interactive() { - $this->hr(); - $this->out(__d('cake_console', "Bake Model\nPath: %s", $this->getPath())); - $this->hr(); - $this->interactive = true; - - $primaryKey = 'id'; - $validate = $associations = array(); - - if (empty($this->connection)) { - $this->connection = $this->DbConfig->getConfig(); - } - $currentModelName = $this->getName(); - $useTable = $this->getTable($currentModelName); - $db = ConnectionManager::getDataSource($this->connection); - $fullTableName = $db->fullTableName($useTable); - if (!in_array($useTable, $this->_tables)) { - $prompt = __d('cake_console', "The table %s doesn't exist or could not be automatically detected\ncontinue anyway?", $useTable); - $continue = $this->in($prompt, array('y', 'n')); - if (strtolower($continue) === 'n') { - return false; - } - } - - $tempModel = new Model(array('name' => $currentModelName, 'table' => $useTable, 'ds' => $this->connection)); - - $knownToExist = false; - try { - $fields = $tempModel->schema(true); - $knownToExist = true; - } catch (Exception $e) { - $fields = array($tempModel->primaryKey); - } - if (!array_key_exists('id', $fields)) { - $primaryKey = $this->findPrimaryKey($fields); - } - $displayField = null; - if ($knownToExist) { - $displayField = $tempModel->hasField(array('name', 'title')); - if (!$displayField) { - $displayField = $this->findDisplayField($tempModel->schema()); - } - - $prompt = __d('cake_console', "Would you like to supply validation criteria \nfor the fields in your model?"); - $wannaDoValidation = $this->in($prompt, array('y', 'n'), 'y'); - if (array_search($useTable, $this->_tables) !== false && strtolower($wannaDoValidation) === 'y') { - $validate = $this->doValidation($tempModel); - } - - $prompt = __d('cake_console', "Would you like to define model associations\n(hasMany, hasOne, belongsTo, etc.)?"); - $wannaDoAssoc = $this->in($prompt, array('y', 'n'), 'y'); - if (strtolower($wannaDoAssoc) === 'y') { - $associations = $this->doAssociations($tempModel); - } - } - - $this->out(); - $this->hr(); - $this->out(__d('cake_console', 'The following Model will be created:')); - $this->hr(); - $this->out(__d('cake_console', "Name: %s", $currentModelName)); - - if ($this->connection !== 'default') { - $this->out(__d('cake_console', "DB Config: %s", $this->connection)); - } - if ($fullTableName !== Inflector::tableize($currentModelName)) { - $this->out(__d('cake_console', 'DB Table: %s', $fullTableName)); - } - if ($primaryKey !== 'id') { - $this->out(__d('cake_console', 'Primary Key: %s', $primaryKey)); - } - if (!empty($validate)) { - $this->out(__d('cake_console', 'Validation: %s', print_r($validate, true))); - } - if (!empty($associations)) { - $this->out(__d('cake_console', 'Associations:')); - $assocKeys = array('belongsTo', 'hasOne', 'hasMany', 'hasAndBelongsToMany'); - foreach ($assocKeys as $assocKey) { - $this->_printAssociation($currentModelName, $assocKey, $associations); - } - } - - $this->hr(); - $looksGood = $this->in(__d('cake_console', 'Look okay?'), array('y', 'n'), 'y'); - - if (strtolower($looksGood) === 'y') { - $vars = compact('associations', 'validate', 'primaryKey', 'useTable', 'displayField'); - $vars['useDbConfig'] = $this->connection; - if ($this->bake($currentModelName, $vars)) { - if ($this->_checkUnitTest()) { - $this->bakeFixture($currentModelName, $useTable); - $this->bakeTest($currentModelName, $useTable, $associations); - } - } - } else { - return false; - } - } - -/** - * Print out all the associations of a particular type - * - * @param string $modelName Name of the model relations belong to. - * @param string $type Name of association you want to see. i.e. 'belongsTo' - * @param string $associations Collection of associations. - * @return void - */ - protected function _printAssociation($modelName, $type, $associations) { - if (!empty($associations[$type])) { - for ($i = 0, $len = count($associations[$type]); $i < $len; $i++) { - $out = "\t" . $modelName . ' ' . $type . ' ' . $associations[$type][$i]['alias']; - $this->out($out); - } - } - } - -/** - * Finds a primary Key in a list of fields. - * - * @param array $fields Array of fields that might have a primary key. - * @return string Name of field that is a primary key. - */ - public function findPrimaryKey($fields) { - $name = 'id'; - foreach ($fields as $name => $field) { - if (isset($field['key']) && $field['key'] === 'primary') { - break; - } - } - return $this->in(__d('cake_console', 'What is the primaryKey?'), null, $name); - } - -/** - * interact with the user to find the displayField value for a model. - * - * @param array $fields Array of fields to look for and choose as a displayField - * @return mixed Name of field to use for displayField or false if the user declines to choose - */ - public function findDisplayField($fields) { - $fieldNames = array_keys($fields); - $prompt = __d('cake_console', "A displayField could not be automatically detected\nwould you like to choose one?"); - $continue = $this->in($prompt, array('y', 'n')); - if (strtolower($continue) === 'n') { - return false; - } - $prompt = __d('cake_console', 'Choose a field from the options above:'); - $choice = $this->inOptions($fieldNames, $prompt); - return $fieldNames[$choice]; - } - -/** - * Handles Generation and user interaction for creating validation. - * - * @param Model $model Model to have validations generated for. - * @return array validate Array of user selected validations. - */ - public function doValidation($model) { - if (!$model instanceof Model) { - return false; - } - - $fields = $model->schema(); - if (empty($fields)) { - return false; - } - - $skipFields = false; - $validate = array(); - $this->initValidations(); - foreach ($fields as $fieldName => $field) { - $validation = $this->fieldValidation($fieldName, $field, $model->primaryKey); - if (isset($validation['_skipFields'])) { - unset($validation['_skipFields']); - $skipFields = true; - } - if (!empty($validation)) { - $validate[$fieldName] = $validation; - } - if ($skipFields) { - return $validate; - } - } - return $validate; - } - -/** - * Populate the _validations array - * - * @return void - */ - public function initValidations() { - $options = $choices = array(); - if (class_exists('Validation')) { - $options = get_class_methods('Validation'); - } - $deprecatedOptions = array('notEmpty', 'between', 'ssn'); - $options = array_diff($options, $deprecatedOptions); - sort($options); - $default = 1; - foreach ($options as $option) { - if ($option{0} !== '_') { - $choices[$default] = $option; - $default++; - } - } - $choices[$default] = 'none'; // Needed since index starts at 1 - $this->_validations = $choices; - return $choices; - } - -/** - * Does individual field validation handling. - * - * @param string $fieldName Name of field to be validated. - * @param array $metaData metadata for field - * @param string $primaryKey The primary key field. - * @return array Array of validation for the field. - */ - public function fieldValidation($fieldName, $metaData, $primaryKey = 'id') { - $defaultChoice = count($this->_validations); - $validate = $alreadyChosen = array(); - - $prompt = __d('cake_console', - "or enter in a valid regex validation string.\nAlternatively [s] skip the rest of the fields.\n" - ); - $methods = array_flip($this->_validations); - - $anotherValidator = 'y'; - while ($anotherValidator === 'y') { - if ($this->interactive) { - $this->out(); - $this->out(__d('cake_console', 'Field: %s', $fieldName)); - $this->out(__d('cake_console', 'Type: %s', $metaData['type'])); - $this->hr(); - $this->out(__d('cake_console', 'Please select one of the following validation options:')); - $this->hr(); - - $optionText = ''; - for ($i = 1, $m = $defaultChoice / 2; $i <= $m; $i++) { - $line = sprintf("%2d. %s", $i, $this->_validations[$i]); - $optionText .= $line . str_repeat(" ", 31 - strlen($line)); - if ($m + $i !== $defaultChoice) { - $optionText .= sprintf("%2d. %s\n", $m + $i, $this->_validations[$m + $i]); - } - } - $this->out($optionText); - $this->out(__d('cake_console', "%s - Do not do any validation on this field.", $defaultChoice)); - $this->hr(); - } - - $guess = $defaultChoice; - if ($metaData['null'] != 1 && !in_array($fieldName, array($primaryKey, 'created', 'modified', 'updated'))) { - if ($fieldName === 'email') { - $guess = $methods['email']; - } elseif ($metaData['type'] === 'string' && $metaData['length'] == 36) { - $guess = $methods['uuid']; - } elseif ($metaData['type'] === 'string') { - $guess = $methods['notBlank']; - } elseif ($metaData['type'] === 'text') { - $guess = $methods['notBlank']; - } elseif ($metaData['type'] === 'integer') { - $guess = $methods['numeric']; - } elseif ($metaData['type'] === 'smallinteger') { - $guess = $methods['numeric']; - } elseif ($metaData['type'] === 'tinyinteger') { - $guess = $methods['numeric']; - } elseif ($metaData['type'] === 'float') { - $guess = $methods['numeric']; - } elseif ($metaData['type'] === 'boolean') { - $guess = $methods['boolean']; - } elseif ($metaData['type'] === 'date') { - $guess = $methods['date']; - } elseif ($metaData['type'] === 'time') { - $guess = $methods['time']; - } elseif ($metaData['type'] === 'datetime') { - $guess = $methods['datetime']; - } elseif ($metaData['type'] === 'inet') { - $guess = $methods['ip']; - } elseif ($metaData['type'] === 'decimal') { - $guess = $methods['decimal']; - } - } - - if ($this->interactive === true) { - $choice = $this->in($prompt, null, $guess); - if ($choice === 's') { - $validate['_skipFields'] = true; - return $validate; - } - if (in_array($choice, $alreadyChosen)) { - $this->out(__d('cake_console', "You have already chosen that validation rule,\nplease choose again")); - continue; - } - if (!isset($this->_validations[$choice]) && is_numeric($choice)) { - $this->out(__d('cake_console', 'Please make a valid selection.')); - continue; - } - $alreadyChosen[] = $choice; - } else { - $choice = $guess; - } - - if (isset($this->_validations[$choice])) { - $validatorName = $this->_validations[$choice]; - } else { - $validatorName = Inflector::slug($choice); - } - - if ($choice != $defaultChoice) { - $validate[$validatorName] = $choice; - if (is_numeric($choice) && isset($this->_validations[$choice])) { - $validate[$validatorName] = $this->_validations[$choice]; - } - } - $anotherValidator = 'n'; - if ($this->interactive && $choice != $defaultChoice) { - $anotherValidator = $this->in(__d('cake_console', "Would you like to add another validation rule\n" . - "or skip the rest of the fields?"), array('y', 'n', 's'), 'n'); - if ($anotherValidator === 's') { - $validate['_skipFields'] = true; - return $validate; - } - } - } - return $validate; - } - -/** - * Handles associations - * - * @param Model $model The model object - * @return array Associations - */ - public function doAssociations($model) { - if (!$model instanceof Model) { - return false; - } - if ($this->interactive === true) { - $this->out(__d('cake_console', 'One moment while the associations are detected.')); - } - - $fields = $model->schema(true); - if (empty($fields)) { - return array(); - } - - if (empty($this->_tables)) { - $this->_tables = (array)$this->getAllTables(); - } - - $associations = array( - 'belongsTo' => array(), - 'hasMany' => array(), - 'hasOne' => array(), - 'hasAndBelongsToMany' => array() - ); - - $associations = $this->findBelongsTo($model, $associations); - $associations = $this->findHasOneAndMany($model, $associations); - $associations = $this->findHasAndBelongsToMany($model, $associations); - - if ($this->interactive !== true) { - unset($associations['hasOne']); - } - - if ($this->interactive === true) { - $this->hr(); - if (empty($associations)) { - $this->out(__d('cake_console', 'None found.')); - } else { - $this->out(__d('cake_console', 'Please confirm the following associations:')); - $this->hr(); - $associations = $this->confirmAssociations($model, $associations); - } - $associations = $this->doMoreAssociations($model, $associations); - } - return $associations; - } - -/** - * Handles behaviors - * - * @param Model $model The model object. - * @return array Behaviors - */ - public function doActsAs($model) { - if (!$model instanceof Model) { - return false; - } - $behaviors = array(); - $fields = $model->schema(true); - if (empty($fields)) { - return array(); - } - - if (isset($fields['lft']) && $fields['lft']['type'] === 'integer' && - isset($fields['rght']) && $fields['rght']['type'] === 'integer' && - isset($fields['parent_id'])) { - $behaviors[] = 'Tree'; - } - return $behaviors; - } - -/** - * Find belongsTo relations and add them to the associations list. - * - * @param Model $model Model instance of model being generated. - * @param array $associations Array of in progress associations - * @return array Associations with belongsTo added in. - */ - public function findBelongsTo(Model $model, $associations) { - $fieldNames = array_keys($model->schema(true)); - foreach ($fieldNames as $fieldName) { - $offset = substr($fieldName, -3) === '_id'; - if ($fieldName != $model->primaryKey && $fieldName !== 'parent_id' && $offset !== false) { - $tmpModelName = $this->_modelNameFromKey($fieldName); - $associations['belongsTo'][] = array( - 'alias' => $tmpModelName, - 'className' => $tmpModelName, - 'foreignKey' => $fieldName, - ); - } elseif ($fieldName === 'parent_id') { - $associations['belongsTo'][] = array( - 'alias' => 'Parent' . $model->name, - 'className' => $model->name, - 'foreignKey' => $fieldName, - ); - } - } - return $associations; - } - -/** - * Find the hasOne and hasMany relations and add them to associations list - * - * @param Model $model Model instance being generated - * @param array $associations Array of in progress associations - * @return array Associations with hasOne and hasMany added in. - */ - public function findHasOneAndMany(Model $model, $associations) { - $foreignKey = $this->_modelKey($model->name); - foreach ($this->_tables as $otherTable) { - $tempOtherModel = $this->_getModelObject($this->_modelName($otherTable), $otherTable); - $tempFieldNames = array_keys($tempOtherModel->schema(true)); - - $pattern = '/_' . preg_quote($model->table, '/') . '|' . preg_quote($model->table, '/') . '_/'; - $possibleJoinTable = preg_match($pattern, $otherTable); - if ($possibleJoinTable) { - continue; - } - foreach ($tempFieldNames as $fieldName) { - $assoc = false; - if ($fieldName !== $model->primaryKey && $fieldName === $foreignKey) { - $assoc = array( - 'alias' => $tempOtherModel->name, - 'className' => $tempOtherModel->name, - 'foreignKey' => $fieldName - ); - } elseif ($otherTable === $model->table && $fieldName === 'parent_id') { - $assoc = array( - 'alias' => 'Child' . $model->name, - 'className' => $model->name, - 'foreignKey' => $fieldName - ); - } - if ($assoc) { - $associations['hasOne'][] = $assoc; - $associations['hasMany'][] = $assoc; - } - - } - } - return $associations; - } - -/** - * Find the hasAndBelongsToMany relations and add them to associations list - * - * @param Model $model Model instance being generated - * @param array $associations Array of in-progress associations - * @return array Associations with hasAndBelongsToMany added in. - */ - public function findHasAndBelongsToMany(Model $model, $associations) { - $foreignKey = $this->_modelKey($model->name); - foreach ($this->_tables as $otherTable) { - $tableName = null; - $offset = strpos($otherTable, $model->table . '_'); - $otherOffset = strpos($otherTable, '_' . $model->table); - - if ($offset !== false) { - $tableName = substr($otherTable, strlen($model->table . '_')); - } elseif ($otherOffset !== false) { - $tableName = substr($otherTable, 0, $otherOffset); - } - if ($tableName && in_array($tableName, $this->_tables)) { - $habtmName = $this->_modelName($tableName); - $associations['hasAndBelongsToMany'][] = array( - 'alias' => $habtmName, - 'className' => $habtmName, - 'foreignKey' => $foreignKey, - 'associationForeignKey' => $this->_modelKey($habtmName), - 'joinTable' => $otherTable - ); - } - } - return $associations; - } - -/** - * Interact with the user and confirm associations. - * - * @param array $model Temporary Model instance. - * @param array $associations Array of associations to be confirmed. - * @return array Array of confirmed associations - */ - public function confirmAssociations(Model $model, $associations) { - foreach ($associations as $type => $settings) { - if (!empty($associations[$type])) { - foreach ($associations[$type] as $i => $assoc) { - $prompt = "{$model->name} {$type} {$assoc['alias']}?"; - $response = $this->in($prompt, array('y', 'n'), 'y'); - - if (strtolower($response) === 'n') { - unset($associations[$type][$i]); - } elseif ($type === 'hasMany') { - unset($associations['hasOne'][$i]); - } - } - $associations[$type] = array_merge($associations[$type]); - } - } - return $associations; - } - -/** - * Interact with the user and generate additional non-conventional associations - * - * @param Model $model Temporary model instance - * @param array $associations Array of associations. - * @return array Array of associations. - */ - public function doMoreAssociations(Model $model, $associations) { - $prompt = __d('cake_console', 'Would you like to define some additional model associations?'); - $wannaDoMoreAssoc = $this->in($prompt, array('y', 'n'), 'n'); - $possibleKeys = $this->_generatePossibleKeys(); - while (strtolower($wannaDoMoreAssoc) === 'y') { - $assocs = array('belongsTo', 'hasOne', 'hasMany', 'hasAndBelongsToMany'); - $this->out(__d('cake_console', 'What is the association type?')); - $assocType = (int)$this->inOptions($assocs, __d('cake_console', 'Enter a number')); - - $this->out(__d('cake_console', "For the following options be very careful to match your setup exactly.\n" . - "Any spelling mistakes will cause errors.")); - $this->hr(); - - $alias = $this->in(__d('cake_console', 'What is the alias for this association?')); - $className = $this->in(__d('cake_console', 'What className will %s use?', $alias), null, $alias); - - if ($assocType === 0) { - if (!empty($possibleKeys[$model->table])) { - $showKeys = $possibleKeys[$model->table]; - } else { - $showKeys = null; - } - $suggestedForeignKey = $this->_modelKey($alias); - } else { - $otherTable = Inflector::tableize($className); - if (in_array($otherTable, $this->_tables)) { - if ($assocType < 3) { - if (!empty($possibleKeys[$otherTable])) { - $showKeys = $possibleKeys[$otherTable]; - } else { - $showKeys = null; - } - } else { - $showKeys = null; - } - } else { - $otherTable = $this->in(__d('cake_console', 'What is the table for this model?')); - $showKeys = $possibleKeys[$otherTable]; - } - $suggestedForeignKey = $this->_modelKey($model->name); - } - if (!empty($showKeys)) { - $this->out(__d('cake_console', 'A helpful List of possible keys')); - $foreignKey = $this->inOptions($showKeys, __d('cake_console', 'What is the foreignKey?')); - $foreignKey = $showKeys[(int)$foreignKey]; - } - if (!isset($foreignKey)) { - $foreignKey = $this->in(__d('cake_console', 'What is the foreignKey? Specify your own.'), null, $suggestedForeignKey); - } - if ($assocType === 3) { - $associationForeignKey = $this->in(__d('cake_console', 'What is the associationForeignKey?'), null, $this->_modelKey($model->name)); - $joinTable = $this->in(__d('cake_console', 'What is the joinTable?')); - } - $associations[$assocs[$assocType]] = array_values((array)$associations[$assocs[$assocType]]); - $count = count($associations[$assocs[$assocType]]); - $i = ($count > 0) ? $count : 0; - $associations[$assocs[$assocType]][$i]['alias'] = $alias; - $associations[$assocs[$assocType]][$i]['className'] = $className; - $associations[$assocs[$assocType]][$i]['foreignKey'] = $foreignKey; - if ($assocType === 3) { - $associations[$assocs[$assocType]][$i]['associationForeignKey'] = $associationForeignKey; - $associations[$assocs[$assocType]][$i]['joinTable'] = $joinTable; - } - $wannaDoMoreAssoc = $this->in(__d('cake_console', 'Define another association?'), array('y', 'n'), 'y'); - } - return $associations; - } - -/** - * Finds all possible keys to use on custom associations. - * - * @return array Array of tables and possible keys - */ - protected function _generatePossibleKeys() { - $possible = array(); - foreach ($this->_tables as $otherTable) { - $tempOtherModel = new Model(array('table' => $otherTable, 'ds' => $this->connection)); - $modelFieldsTemp = $tempOtherModel->schema(true); - foreach ($modelFieldsTemp as $fieldName => $field) { - if ($field['type'] === 'integer' || $field['type'] === 'string') { - $possible[$otherTable][] = $fieldName; - } - } - } - return $possible; - } - -/** - * Assembles and writes a Model file. - * - * @param string|object $name Model name or object - * @param array|bool $data if array and $name is not an object assume bake data, otherwise boolean. - * @return string - */ - public function bake($name, $data = array()) { - if ($name instanceof Model) { - if (!$data) { - $data = array(); - $data['associations'] = $this->doAssociations($name); - $data['validate'] = $this->doValidation($name); - $data['actsAs'] = $this->doActsAs($name); - } - $data['primaryKey'] = $name->primaryKey; - $data['useTable'] = $name->table; - $data['useDbConfig'] = $name->useDbConfig; - $data['name'] = $name = $name->name; - } else { - $data['name'] = $name; - } - - $defaults = array( - 'associations' => array(), - 'actsAs' => array(), - 'validate' => array(), - 'primaryKey' => 'id', - 'useTable' => null, - 'useDbConfig' => 'default', - 'displayField' => null - ); - $data = array_merge($defaults, $data); - - $pluginPath = ''; - if ($this->plugin) { - $pluginPath = $this->plugin . '.'; - } - - $this->Template->set($data); - $this->Template->set(array( - 'plugin' => $this->plugin, - 'pluginPath' => $pluginPath - )); - $out = $this->Template->generate('classes', 'model'); - - $path = $this->getPath(); - $filename = $path . $name . '.php'; - $this->out("\n" . __d('cake_console', 'Baking model class for %s...', $name), 1, Shell::QUIET); - $this->createFile($filename, $out); - ClassRegistry::flush(); - return $out; - } - -/** - * Assembles and writes a unit test file - * - * @param string $className Model class name - * @return string - */ - public function bakeTest($className) { - $this->Test->interactive = $this->interactive; - $this->Test->plugin = $this->plugin; - $this->Test->connection = $this->connection; - return $this->Test->bake('Model', $className); - } - -/** - * outputs the a list of possible models or controllers from database - * - * @param string $useDbConfig Database configuration name - * @return array - */ - public function listAll($useDbConfig = null) { - $this->_tables = $this->getAllTables($useDbConfig); - - $this->_modelNames = array(); - $count = count($this->_tables); - for ($i = 0; $i < $count; $i++) { - $this->_modelNames[] = $this->_modelName($this->_tables[$i]); - } - if ($this->interactive === true) { - $this->out(__d('cake_console', 'Possible Models based on your current database:')); - $len = strlen($count + 1); - for ($i = 0; $i < $count; $i++) { - $this->out(sprintf("%${len}d. %s", $i + 1, $this->_modelNames[$i])); - } - } - return $this->_tables; - } - -/** - * Interact with the user to determine the table name of a particular model - * - * @param string $modelName Name of the model you want a table for. - * @param string $useDbConfig Name of the database config you want to get tables from. - * @return string Table name - */ - public function getTable($modelName, $useDbConfig = null) { - $useTable = Inflector::tableize($modelName); - if (in_array($modelName, $this->_modelNames)) { - $modelNames = array_flip($this->_modelNames); - $useTable = $this->_tables[$modelNames[$modelName]]; - } - - if ($this->interactive === true) { - if (!isset($useDbConfig)) { - $useDbConfig = $this->connection; - } - $db = ConnectionManager::getDataSource($useDbConfig); - $fullTableName = $db->fullTableName($useTable, false); - $tableIsGood = false; - if (array_search($useTable, $this->_tables) === false) { - $this->out(); - $this->out(__d('cake_console', "Given your model named '%s',\nCake would expect a database table named '%s'", $modelName, $fullTableName)); - $tableIsGood = $this->in(__d('cake_console', 'Do you want to use this table?'), array('y', 'n'), 'y'); - } - if (strtolower($tableIsGood) === 'n') { - $useTable = $this->in(__d('cake_console', 'What is the name of the table (without prefix)?')); - } - } - return $useTable; - } - -/** - * Get an Array of all the tables in the supplied connection - * will halt the script if no tables are found. - * - * @param string $useDbConfig Connection name to scan. - * @return array Array of tables in the database. - */ - public function getAllTables($useDbConfig = null) { - if (!isset($useDbConfig)) { - $useDbConfig = $this->connection; - } - - $tables = array(); - $db = ConnectionManager::getDataSource($useDbConfig); - $db->cacheSources = false; - $usePrefix = empty($db->config['prefix']) ? '' : $db->config['prefix']; - if ($usePrefix) { - foreach ($db->listSources() as $table) { - if (!strncmp($table, $usePrefix, strlen($usePrefix))) { - $tables[] = substr($table, strlen($usePrefix)); - } - } - } else { - $tables = $db->listSources(); - } - if (empty($tables)) { - $this->err(__d('cake_console', 'Your database does not have any tables.')); - return $this->_stop(); - } - sort($tables); - return $tables; - } - -/** - * Forces the user to specify the model he wants to bake, and returns the selected model name. - * - * @param string $useDbConfig Database config name - * @return string The model name - */ - public function getName($useDbConfig = null) { - $this->listAll($useDbConfig); - - $enteredModel = ''; - - while (!$enteredModel) { - $enteredModel = $this->in(__d('cake_console', "Enter a number from the list above,\n" . - "type in the name of another model, or 'q' to exit"), null, 'q'); - - if ($enteredModel === 'q') { - $this->out(__d('cake_console', 'Exit')); - return $this->_stop(); - } - - if (!$enteredModel || (int)$enteredModel > count($this->_modelNames)) { - $this->err(__d('cake_console', "The model name you supplied was empty,\n" . - "or the number you selected was not an option. Please try again.")); - $enteredModel = ''; - } - } - if ((int)$enteredModel > 0 && (int)$enteredModel <= count($this->_modelNames)) { - return $this->_modelNames[(int)$enteredModel - 1]; - } - - return $enteredModel; - } - -/** - * Gets the option parser instance and configures it. - * - * @return ConsoleOptionParser - */ - public function getOptionParser() { - $parser = parent::getOptionParser(); - - $parser->description( - __d('cake_console', 'Bake models.') - )->addArgument('name', array( - 'help' => __d('cake_console', 'Name of the model to bake. Can use Plugin.name to bake plugin models.') - ))->addSubcommand('all', array( - 'help' => __d('cake_console', 'Bake all model files with associations and validation.') - ))->addOption('plugin', array( - 'short' => 'p', - 'help' => __d('cake_console', 'Plugin to bake the model into.') - ))->addOption('theme', array( - 'short' => 't', - 'help' => __d('cake_console', 'Theme to use when baking code.') - ))->addOption('connection', array( - 'short' => 'c', - 'help' => __d('cake_console', 'The connection the model table is on.') - ))->addOption('force', array( - 'short' => 'f', - 'help' => __d('cake_console', 'Force overwriting existing files without prompting.') - ))->epilog( - __d('cake_console', 'Omitting all arguments and options will enter into an interactive mode.') - ); - - return $parser; - } - -/** - * Interact with FixtureTask to automatically bake fixtures when baking models. - * - * @param string $className Name of class to bake fixture for - * @param string $useTable Optional table name for fixture to use. - * @return void - * @see FixtureTask::bake - */ - public function bakeFixture($className, $useTable = null) { - $this->Fixture->interactive = $this->interactive; - $this->Fixture->connection = $this->connection; - $this->Fixture->plugin = $this->plugin; - $this->Fixture->bake($className, $useTable); - } +class ModelTask extends BakeTask +{ + + /** + * path to Model directory + * + * @var string + */ + public $path = null; + + /** + * tasks + * + * @var array + */ + public $tasks = ['DbConfig', 'Fixture', 'Test', 'Template']; + + /** + * Tables to skip when running all() + * + * @var array + */ + public $skipTables = ['i18n']; + + /** + * Holds tables found on connection. + * + * @var array + */ + protected $_tables = []; + + /** + * Holds the model names + * + * @var array + */ + protected $_modelNames = []; + + /** + * Holds validation method map. + * + * @var array + */ + protected $_validations = []; + + /** + * Override initialize + * + * @return void + */ + public function initialize() + { + $this->path = current(App::path('Model')); + } + + /** + * Execution method always used for tasks + * + * @return void + */ + public function execute() + { + parent::execute(); + + if (empty($this->args)) { + $this->_interactive(); + } + + if (!empty($this->args[0])) { + $this->interactive = false; + if (!isset($this->connection)) { + $this->connection = 'default'; + } + if (strtolower($this->args[0]) === 'all') { + return $this->all(); + } + $model = $this->_modelName($this->args[0]); + $this->listAll($this->connection); + $useTable = $this->getTable($model); + $object = $this->_getModelObject($model, $useTable); + if ($this->bake($object, false)) { + if ($this->_checkUnitTest()) { + $this->bakeFixture($model, $useTable); + $this->bakeTest($model); + } + } + } + } + + /** + * Handles interactive baking + * + * @return bool + */ + protected function _interactive() + { + $this->hr(); + $this->out(__d('cake_console', "Bake Model\nPath: %s", $this->getPath())); + $this->hr(); + $this->interactive = true; + + $primaryKey = 'id'; + $validate = $associations = []; + + if (empty($this->connection)) { + $this->connection = $this->DbConfig->getConfig(); + } + $currentModelName = $this->getName(); + $useTable = $this->getTable($currentModelName); + $db = ConnectionManager::getDataSource($this->connection); + $fullTableName = $db->fullTableName($useTable); + if (!in_array($useTable, $this->_tables)) { + $prompt = __d('cake_console', "The table %s doesn't exist or could not be automatically detected\ncontinue anyway?", $useTable); + $continue = $this->in($prompt, ['y', 'n']); + if (strtolower($continue) === 'n') { + return false; + } + } + + $tempModel = new Model(['name' => $currentModelName, 'table' => $useTable, 'ds' => $this->connection]); + + $knownToExist = false; + try { + $fields = $tempModel->schema(true); + $knownToExist = true; + } catch (Exception $e) { + $fields = [$tempModel->primaryKey]; + } + if (!array_key_exists('id', $fields)) { + $primaryKey = $this->findPrimaryKey($fields); + } + $displayField = null; + if ($knownToExist) { + $displayField = $tempModel->hasField(['name', 'title']); + if (!$displayField) { + $displayField = $this->findDisplayField($tempModel->schema()); + } + + $prompt = __d('cake_console', "Would you like to supply validation criteria \nfor the fields in your model?"); + $wannaDoValidation = $this->in($prompt, ['y', 'n'], 'y'); + if (array_search($useTable, $this->_tables) !== false && strtolower($wannaDoValidation) === 'y') { + $validate = $this->doValidation($tempModel); + } + + $prompt = __d('cake_console', "Would you like to define model associations\n(hasMany, hasOne, belongsTo, etc.)?"); + $wannaDoAssoc = $this->in($prompt, ['y', 'n'], 'y'); + if (strtolower($wannaDoAssoc) === 'y') { + $associations = $this->doAssociations($tempModel); + } + } + + $this->out(); + $this->hr(); + $this->out(__d('cake_console', 'The following Model will be created:')); + $this->hr(); + $this->out(__d('cake_console', "Name: %s", $currentModelName)); + + if ($this->connection !== 'default') { + $this->out(__d('cake_console', "DB Config: %s", $this->connection)); + } + if ($fullTableName !== Inflector::tableize($currentModelName)) { + $this->out(__d('cake_console', 'DB Table: %s', $fullTableName)); + } + if ($primaryKey !== 'id') { + $this->out(__d('cake_console', 'Primary Key: %s', $primaryKey)); + } + if (!empty($validate)) { + $this->out(__d('cake_console', 'Validation: %s', print_r($validate, true))); + } + if (!empty($associations)) { + $this->out(__d('cake_console', 'Associations:')); + $assocKeys = ['belongsTo', 'hasOne', 'hasMany', 'hasAndBelongsToMany']; + foreach ($assocKeys as $assocKey) { + $this->_printAssociation($currentModelName, $assocKey, $associations); + } + } + + $this->hr(); + $looksGood = $this->in(__d('cake_console', 'Look okay?'), ['y', 'n'], 'y'); + + if (strtolower($looksGood) === 'y') { + $vars = compact('associations', 'validate', 'primaryKey', 'useTable', 'displayField'); + $vars['useDbConfig'] = $this->connection; + if ($this->bake($currentModelName, $vars)) { + if ($this->_checkUnitTest()) { + $this->bakeFixture($currentModelName, $useTable); + $this->bakeTest($currentModelName, $useTable, $associations); + } + } + } else { + return false; + } + } + + /** + * Forces the user to specify the model he wants to bake, and returns the selected model name. + * + * @param string $useDbConfig Database config name + * @return string The model name + */ + public function getName($useDbConfig = null) + { + $this->listAll($useDbConfig); + + $enteredModel = ''; + + while (!$enteredModel) { + $enteredModel = $this->in(__d('cake_console', "Enter a number from the list above,\n" . + "type in the name of another model, or 'q' to exit"), null, 'q'); + + if ($enteredModel === 'q') { + $this->out(__d('cake_console', 'Exit')); + return $this->_stop(); + } + + if (!$enteredModel || (int)$enteredModel > count($this->_modelNames)) { + $this->err(__d('cake_console', "The model name you supplied was empty,\n" . + "or the number you selected was not an option. Please try again.")); + $enteredModel = ''; + } + } + if ((int)$enteredModel > 0 && (int)$enteredModel <= count($this->_modelNames)) { + return $this->_modelNames[(int)$enteredModel - 1]; + } + + return $enteredModel; + } + + /** + * outputs the a list of possible models or controllers from database + * + * @param string $useDbConfig Database configuration name + * @return array + */ + public function listAll($useDbConfig = null) + { + $this->_tables = $this->getAllTables($useDbConfig); + + $this->_modelNames = []; + $count = count($this->_tables); + for ($i = 0; $i < $count; $i++) { + $this->_modelNames[] = $this->_modelName($this->_tables[$i]); + } + if ($this->interactive === true) { + $this->out(__d('cake_console', 'Possible Models based on your current database:')); + $len = strlen($count + 1); + for ($i = 0; $i < $count; $i++) { + $this->out(sprintf("%${len}d. %s", $i + 1, $this->_modelNames[$i])); + } + } + return $this->_tables; + } + + /** + * Get an Array of all the tables in the supplied connection + * will halt the script if no tables are found. + * + * @param string $useDbConfig Connection name to scan. + * @return array Array of tables in the database. + */ + public function getAllTables($useDbConfig = null) + { + if (!isset($useDbConfig)) { + $useDbConfig = $this->connection; + } + + $tables = []; + $db = ConnectionManager::getDataSource($useDbConfig); + $db->cacheSources = false; + $usePrefix = empty($db->config['prefix']) ? '' : $db->config['prefix']; + if ($usePrefix) { + foreach ($db->listSources() as $table) { + if (!strncmp($table, $usePrefix, strlen($usePrefix))) { + $tables[] = substr($table, strlen($usePrefix)); + } + } + } else { + $tables = $db->listSources(); + } + if (empty($tables)) { + $this->err(__d('cake_console', 'Your database does not have any tables.')); + return $this->_stop(); + } + sort($tables); + return $tables; + } + + /** + * Interact with the user to determine the table name of a particular model + * + * @param string $modelName Name of the model you want a table for. + * @param string $useDbConfig Name of the database config you want to get tables from. + * @return string Table name + */ + public function getTable($modelName, $useDbConfig = null) + { + $useTable = Inflector::tableize($modelName); + if (in_array($modelName, $this->_modelNames)) { + $modelNames = array_flip($this->_modelNames); + $useTable = $this->_tables[$modelNames[$modelName]]; + } + + if ($this->interactive === true) { + if (!isset($useDbConfig)) { + $useDbConfig = $this->connection; + } + $db = ConnectionManager::getDataSource($useDbConfig); + $fullTableName = $db->fullTableName($useTable, false); + $tableIsGood = false; + if (array_search($useTable, $this->_tables) === false) { + $this->out(); + $this->out(__d('cake_console', "Given your model named '%s',\nCake would expect a database table named '%s'", $modelName, $fullTableName)); + $tableIsGood = $this->in(__d('cake_console', 'Do you want to use this table?'), ['y', 'n'], 'y'); + } + if (strtolower($tableIsGood) === 'n') { + $useTable = $this->in(__d('cake_console', 'What is the name of the table (without prefix)?')); + } + } + return $useTable; + } + + /** + * Finds a primary Key in a list of fields. + * + * @param array $fields Array of fields that might have a primary key. + * @return string Name of field that is a primary key. + */ + public function findPrimaryKey($fields) + { + $name = 'id'; + foreach ($fields as $name => $field) { + if (isset($field['key']) && $field['key'] === 'primary') { + break; + } + } + return $this->in(__d('cake_console', 'What is the primaryKey?'), null, $name); + } + + /** + * interact with the user to find the displayField value for a model. + * + * @param array $fields Array of fields to look for and choose as a displayField + * @return mixed Name of field to use for displayField or false if the user declines to choose + */ + public function findDisplayField($fields) + { + $fieldNames = array_keys($fields); + $prompt = __d('cake_console', "A displayField could not be automatically detected\nwould you like to choose one?"); + $continue = $this->in($prompt, ['y', 'n']); + if (strtolower($continue) === 'n') { + return false; + } + $prompt = __d('cake_console', 'Choose a field from the options above:'); + $choice = $this->inOptions($fieldNames, $prompt); + return $fieldNames[$choice]; + } + + /** + * Generate a key value list of options and a prompt. + * + * @param array $options Array of options to use for the selections. indexes must start at 0 + * @param string $prompt Prompt to use for options list. + * @param int $default The default option for the given prompt. + * @return int Result of user choice. + */ + public function inOptions($options, $prompt = null, $default = null) + { + $valid = false; + $max = count($options); + while (!$valid) { + $len = strlen(count($options) + 1); + foreach ($options as $i => $option) { + $this->out(sprintf("%${len}d. %s", $i + 1, $option)); + } + if (empty($prompt)) { + $prompt = __d('cake_console', 'Make a selection from the choices above'); + } + $choice = $this->in($prompt, null, $default); + if ((int)$choice > 0 && (int)$choice <= $max) { + $valid = true; + } + } + return $choice - 1; + } + + /** + * Handles Generation and user interaction for creating validation. + * + * @param Model $model Model to have validations generated for. + * @return array validate Array of user selected validations. + */ + public function doValidation($model) + { + if (!$model instanceof Model) { + return false; + } + + $fields = $model->schema(); + if (empty($fields)) { + return false; + } + + $skipFields = false; + $validate = []; + $this->initValidations(); + foreach ($fields as $fieldName => $field) { + $validation = $this->fieldValidation($fieldName, $field, $model->primaryKey); + if (isset($validation['_skipFields'])) { + unset($validation['_skipFields']); + $skipFields = true; + } + if (!empty($validation)) { + $validate[$fieldName] = $validation; + } + if ($skipFields) { + return $validate; + } + } + return $validate; + } + + /** + * Populate the _validations array + * + * @return void + */ + public function initValidations() + { + $options = $choices = []; + if (class_exists('Validation')) { + $options = get_class_methods('Validation'); + } + $deprecatedOptions = ['notEmpty', 'between', 'ssn']; + $options = array_diff($options, $deprecatedOptions); + sort($options); + $default = 1; + foreach ($options as $option) { + if ($option{0} !== '_') { + $choices[$default] = $option; + $default++; + } + } + $choices[$default] = 'none'; // Needed since index starts at 1 + $this->_validations = $choices; + return $choices; + } + + /** + * Does individual field validation handling. + * + * @param string $fieldName Name of field to be validated. + * @param array $metaData metadata for field + * @param string $primaryKey The primary key field. + * @return array Array of validation for the field. + */ + public function fieldValidation($fieldName, $metaData, $primaryKey = 'id') + { + $defaultChoice = count($this->_validations); + $validate = $alreadyChosen = []; + + $prompt = __d('cake_console', + "or enter in a valid regex validation string.\nAlternatively [s] skip the rest of the fields.\n" + ); + $methods = array_flip($this->_validations); + + $anotherValidator = 'y'; + while ($anotherValidator === 'y') { + if ($this->interactive) { + $this->out(); + $this->out(__d('cake_console', 'Field: %s', $fieldName)); + $this->out(__d('cake_console', 'Type: %s', $metaData['type'])); + $this->hr(); + $this->out(__d('cake_console', 'Please select one of the following validation options:')); + $this->hr(); + + $optionText = ''; + for ($i = 1, $m = $defaultChoice / 2; $i <= $m; $i++) { + $line = sprintf("%2d. %s", $i, $this->_validations[$i]); + $optionText .= $line . str_repeat(" ", 31 - strlen($line)); + if ($m + $i !== $defaultChoice) { + $optionText .= sprintf("%2d. %s\n", $m + $i, $this->_validations[$m + $i]); + } + } + $this->out($optionText); + $this->out(__d('cake_console', "%s - Do not do any validation on this field.", $defaultChoice)); + $this->hr(); + } + + $guess = $defaultChoice; + if ($metaData['null'] != 1 && !in_array($fieldName, [$primaryKey, 'created', 'modified', 'updated'])) { + if ($fieldName === 'email') { + $guess = $methods['email']; + } else if ($metaData['type'] === 'string' && $metaData['length'] == 36) { + $guess = $methods['uuid']; + } else if ($metaData['type'] === 'string') { + $guess = $methods['notBlank']; + } else if ($metaData['type'] === 'text') { + $guess = $methods['notBlank']; + } else if ($metaData['type'] === 'integer') { + $guess = $methods['numeric']; + } else if ($metaData['type'] === 'smallinteger') { + $guess = $methods['numeric']; + } else if ($metaData['type'] === 'tinyinteger') { + $guess = $methods['numeric']; + } else if ($metaData['type'] === 'float') { + $guess = $methods['numeric']; + } else if ($metaData['type'] === 'boolean') { + $guess = $methods['boolean']; + } else if ($metaData['type'] === 'date') { + $guess = $methods['date']; + } else if ($metaData['type'] === 'time') { + $guess = $methods['time']; + } else if ($metaData['type'] === 'datetime') { + $guess = $methods['datetime']; + } else if ($metaData['type'] === 'inet') { + $guess = $methods['ip']; + } else if ($metaData['type'] === 'decimal') { + $guess = $methods['decimal']; + } + } + + if ($this->interactive === true) { + $choice = $this->in($prompt, null, $guess); + if ($choice === 's') { + $validate['_skipFields'] = true; + return $validate; + } + if (in_array($choice, $alreadyChosen)) { + $this->out(__d('cake_console', "You have already chosen that validation rule,\nplease choose again")); + continue; + } + if (!isset($this->_validations[$choice]) && is_numeric($choice)) { + $this->out(__d('cake_console', 'Please make a valid selection.')); + continue; + } + $alreadyChosen[] = $choice; + } else { + $choice = $guess; + } + + if (isset($this->_validations[$choice])) { + $validatorName = $this->_validations[$choice]; + } else { + $validatorName = Inflector::slug($choice); + } + + if ($choice != $defaultChoice) { + $validate[$validatorName] = $choice; + if (is_numeric($choice) && isset($this->_validations[$choice])) { + $validate[$validatorName] = $this->_validations[$choice]; + } + } + $anotherValidator = 'n'; + if ($this->interactive && $choice != $defaultChoice) { + $anotherValidator = $this->in(__d('cake_console', "Would you like to add another validation rule\n" . + "or skip the rest of the fields?"), ['y', 'n', 's'], 'n'); + if ($anotherValidator === 's') { + $validate['_skipFields'] = true; + return $validate; + } + } + } + return $validate; + } + + /** + * Handles associations + * + * @param Model $model The model object + * @return array Associations + */ + public function doAssociations($model) + { + if (!$model instanceof Model) { + return false; + } + if ($this->interactive === true) { + $this->out(__d('cake_console', 'One moment while the associations are detected.')); + } + + $fields = $model->schema(true); + if (empty($fields)) { + return []; + } + + if (empty($this->_tables)) { + $this->_tables = (array)$this->getAllTables(); + } + + $associations = [ + 'belongsTo' => [], + 'hasMany' => [], + 'hasOne' => [], + 'hasAndBelongsToMany' => [] + ]; + + $associations = $this->findBelongsTo($model, $associations); + $associations = $this->findHasOneAndMany($model, $associations); + $associations = $this->findHasAndBelongsToMany($model, $associations); + + if ($this->interactive !== true) { + unset($associations['hasOne']); + } + + if ($this->interactive === true) { + $this->hr(); + if (empty($associations)) { + $this->out(__d('cake_console', 'None found.')); + } else { + $this->out(__d('cake_console', 'Please confirm the following associations:')); + $this->hr(); + $associations = $this->confirmAssociations($model, $associations); + } + $associations = $this->doMoreAssociations($model, $associations); + } + return $associations; + } + + /** + * Find belongsTo relations and add them to the associations list. + * + * @param Model $model Model instance of model being generated. + * @param array $associations Array of in progress associations + * @return array Associations with belongsTo added in. + */ + public function findBelongsTo(Model $model, $associations) + { + $fieldNames = array_keys($model->schema(true)); + foreach ($fieldNames as $fieldName) { + $offset = substr($fieldName, -3) === '_id'; + if ($fieldName != $model->primaryKey && $fieldName !== 'parent_id' && $offset !== false) { + $tmpModelName = $this->_modelNameFromKey($fieldName); + $associations['belongsTo'][] = [ + 'alias' => $tmpModelName, + 'className' => $tmpModelName, + 'foreignKey' => $fieldName, + ]; + } else if ($fieldName === 'parent_id') { + $associations['belongsTo'][] = [ + 'alias' => 'Parent' . $model->name, + 'className' => $model->name, + 'foreignKey' => $fieldName, + ]; + } + } + return $associations; + } + + /** + * Find the hasOne and hasMany relations and add them to associations list + * + * @param Model $model Model instance being generated + * @param array $associations Array of in progress associations + * @return array Associations with hasOne and hasMany added in. + */ + public function findHasOneAndMany(Model $model, $associations) + { + $foreignKey = $this->_modelKey($model->name); + foreach ($this->_tables as $otherTable) { + $tempOtherModel = $this->_getModelObject($this->_modelName($otherTable), $otherTable); + $tempFieldNames = array_keys($tempOtherModel->schema(true)); + + $pattern = '/_' . preg_quote($model->table, '/') . '|' . preg_quote($model->table, '/') . '_/'; + $possibleJoinTable = preg_match($pattern, $otherTable); + if ($possibleJoinTable) { + continue; + } + foreach ($tempFieldNames as $fieldName) { + $assoc = false; + if ($fieldName !== $model->primaryKey && $fieldName === $foreignKey) { + $assoc = [ + 'alias' => $tempOtherModel->name, + 'className' => $tempOtherModel->name, + 'foreignKey' => $fieldName + ]; + } else if ($otherTable === $model->table && $fieldName === 'parent_id') { + $assoc = [ + 'alias' => 'Child' . $model->name, + 'className' => $model->name, + 'foreignKey' => $fieldName + ]; + } + if ($assoc) { + $associations['hasOne'][] = $assoc; + $associations['hasMany'][] = $assoc; + } + + } + } + return $associations; + } + + /** + * Get a model object for a class name. + * + * @param string $className Name of class you want model to be. + * @param string $table Table name + * @return Model Model instance + */ + protected function _getModelObject($className, $table = null) + { + if (!$table) { + $table = Inflector::tableize($className); + } + $object = new Model(['name' => $className, 'table' => $table, 'ds' => $this->connection]); + $fields = $object->schema(true); + foreach ($fields as $name => $field) { + if (isset($field['key']) && $field['key'] === 'primary') { + $object->primaryKey = $name; + break; + } + } + return $object; + } + + /** + * Find the hasAndBelongsToMany relations and add them to associations list + * + * @param Model $model Model instance being generated + * @param array $associations Array of in-progress associations + * @return array Associations with hasAndBelongsToMany added in. + */ + public function findHasAndBelongsToMany(Model $model, $associations) + { + $foreignKey = $this->_modelKey($model->name); + foreach ($this->_tables as $otherTable) { + $tableName = null; + $offset = strpos($otherTable, $model->table . '_'); + $otherOffset = strpos($otherTable, '_' . $model->table); + + if ($offset !== false) { + $tableName = substr($otherTable, strlen($model->table . '_')); + } else if ($otherOffset !== false) { + $tableName = substr($otherTable, 0, $otherOffset); + } + if ($tableName && in_array($tableName, $this->_tables)) { + $habtmName = $this->_modelName($tableName); + $associations['hasAndBelongsToMany'][] = [ + 'alias' => $habtmName, + 'className' => $habtmName, + 'foreignKey' => $foreignKey, + 'associationForeignKey' => $this->_modelKey($habtmName), + 'joinTable' => $otherTable + ]; + } + } + return $associations; + } + + /** + * Interact with the user and confirm associations. + * + * @param array $model Temporary Model instance. + * @param array $associations Array of associations to be confirmed. + * @return array Array of confirmed associations + */ + public function confirmAssociations(Model $model, $associations) + { + foreach ($associations as $type => $settings) { + if (!empty($associations[$type])) { + foreach ($associations[$type] as $i => $assoc) { + $prompt = "{$model->name} {$type} {$assoc['alias']}?"; + $response = $this->in($prompt, ['y', 'n'], 'y'); + + if (strtolower($response) === 'n') { + unset($associations[$type][$i]); + } else if ($type === 'hasMany') { + unset($associations['hasOne'][$i]); + } + } + $associations[$type] = array_merge($associations[$type]); + } + } + return $associations; + } + + /** + * Interact with the user and generate additional non-conventional associations + * + * @param Model $model Temporary model instance + * @param array $associations Array of associations. + * @return array Array of associations. + */ + public function doMoreAssociations(Model $model, $associations) + { + $prompt = __d('cake_console', 'Would you like to define some additional model associations?'); + $wannaDoMoreAssoc = $this->in($prompt, ['y', 'n'], 'n'); + $possibleKeys = $this->_generatePossibleKeys(); + while (strtolower($wannaDoMoreAssoc) === 'y') { + $assocs = ['belongsTo', 'hasOne', 'hasMany', 'hasAndBelongsToMany']; + $this->out(__d('cake_console', 'What is the association type?')); + $assocType = (int)$this->inOptions($assocs, __d('cake_console', 'Enter a number')); + + $this->out(__d('cake_console', "For the following options be very careful to match your setup exactly.\n" . + "Any spelling mistakes will cause errors.")); + $this->hr(); + + $alias = $this->in(__d('cake_console', 'What is the alias for this association?')); + $className = $this->in(__d('cake_console', 'What className will %s use?', $alias), null, $alias); + + if ($assocType === 0) { + if (!empty($possibleKeys[$model->table])) { + $showKeys = $possibleKeys[$model->table]; + } else { + $showKeys = null; + } + $suggestedForeignKey = $this->_modelKey($alias); + } else { + $otherTable = Inflector::tableize($className); + if (in_array($otherTable, $this->_tables)) { + if ($assocType < 3) { + if (!empty($possibleKeys[$otherTable])) { + $showKeys = $possibleKeys[$otherTable]; + } else { + $showKeys = null; + } + } else { + $showKeys = null; + } + } else { + $otherTable = $this->in(__d('cake_console', 'What is the table for this model?')); + $showKeys = $possibleKeys[$otherTable]; + } + $suggestedForeignKey = $this->_modelKey($model->name); + } + if (!empty($showKeys)) { + $this->out(__d('cake_console', 'A helpful List of possible keys')); + $foreignKey = $this->inOptions($showKeys, __d('cake_console', 'What is the foreignKey?')); + $foreignKey = $showKeys[(int)$foreignKey]; + } + if (!isset($foreignKey)) { + $foreignKey = $this->in(__d('cake_console', 'What is the foreignKey? Specify your own.'), null, $suggestedForeignKey); + } + if ($assocType === 3) { + $associationForeignKey = $this->in(__d('cake_console', 'What is the associationForeignKey?'), null, $this->_modelKey($model->name)); + $joinTable = $this->in(__d('cake_console', 'What is the joinTable?')); + } + $associations[$assocs[$assocType]] = array_values((array)$associations[$assocs[$assocType]]); + $count = count($associations[$assocs[$assocType]]); + $i = ($count > 0) ? $count : 0; + $associations[$assocs[$assocType]][$i]['alias'] = $alias; + $associations[$assocs[$assocType]][$i]['className'] = $className; + $associations[$assocs[$assocType]][$i]['foreignKey'] = $foreignKey; + if ($assocType === 3) { + $associations[$assocs[$assocType]][$i]['associationForeignKey'] = $associationForeignKey; + $associations[$assocs[$assocType]][$i]['joinTable'] = $joinTable; + } + $wannaDoMoreAssoc = $this->in(__d('cake_console', 'Define another association?'), ['y', 'n'], 'y'); + } + return $associations; + } + + /** + * Finds all possible keys to use on custom associations. + * + * @return array Array of tables and possible keys + */ + protected function _generatePossibleKeys() + { + $possible = []; + foreach ($this->_tables as $otherTable) { + $tempOtherModel = new Model(['table' => $otherTable, 'ds' => $this->connection]); + $modelFieldsTemp = $tempOtherModel->schema(true); + foreach ($modelFieldsTemp as $fieldName => $field) { + if ($field['type'] === 'integer' || $field['type'] === 'string') { + $possible[$otherTable][] = $fieldName; + } + } + } + return $possible; + } + + /** + * Print out all the associations of a particular type + * + * @param string $modelName Name of the model relations belong to. + * @param string $type Name of association you want to see. i.e. 'belongsTo' + * @param string $associations Collection of associations. + * @return void + */ + protected function _printAssociation($modelName, $type, $associations) + { + if (!empty($associations[$type])) { + for ($i = 0, $len = count($associations[$type]); $i < $len; $i++) { + $out = "\t" . $modelName . ' ' . $type . ' ' . $associations[$type][$i]['alias']; + $this->out($out); + } + } + } + + /** + * Assembles and writes a Model file. + * + * @param string|object $name Model name or object + * @param array|bool $data if array and $name is not an object assume bake data, otherwise boolean. + * @return string + */ + public function bake($name, $data = []) + { + if ($name instanceof Model) { + if (!$data) { + $data = []; + $data['associations'] = $this->doAssociations($name); + $data['validate'] = $this->doValidation($name); + $data['actsAs'] = $this->doActsAs($name); + } + $data['primaryKey'] = $name->primaryKey; + $data['useTable'] = $name->table; + $data['useDbConfig'] = $name->useDbConfig; + $data['name'] = $name = $name->name; + } else { + $data['name'] = $name; + } + + $defaults = [ + 'associations' => [], + 'actsAs' => [], + 'validate' => [], + 'primaryKey' => 'id', + 'useTable' => null, + 'useDbConfig' => 'default', + 'displayField' => null + ]; + $data = array_merge($defaults, $data); + + $pluginPath = ''; + if ($this->plugin) { + $pluginPath = $this->plugin . '.'; + } + + $this->Template->set($data); + $this->Template->set([ + 'plugin' => $this->plugin, + 'pluginPath' => $pluginPath + ]); + $out = $this->Template->generate('classes', 'model'); + + $path = $this->getPath(); + $filename = $path . $name . '.php'; + $this->out("\n" . __d('cake_console', 'Baking model class for %s...', $name), 1, Shell::QUIET); + $this->createFile($filename, $out); + ClassRegistry::flush(); + return $out; + } + + /** + * Handles behaviors + * + * @param Model $model The model object. + * @return array Behaviors + */ + public function doActsAs($model) + { + if (!$model instanceof Model) { + return false; + } + $behaviors = []; + $fields = $model->schema(true); + if (empty($fields)) { + return []; + } + + if (isset($fields['lft']) && $fields['lft']['type'] === 'integer' && + isset($fields['rght']) && $fields['rght']['type'] === 'integer' && + isset($fields['parent_id'])) { + $behaviors[] = 'Tree'; + } + return $behaviors; + } + + /** + * Interact with FixtureTask to automatically bake fixtures when baking models. + * + * @param string $className Name of class to bake fixture for + * @param string $useTable Optional table name for fixture to use. + * @return void + * @see FixtureTask::bake + */ + public function bakeFixture($className, $useTable = null) + { + $this->Fixture->interactive = $this->interactive; + $this->Fixture->connection = $this->connection; + $this->Fixture->plugin = $this->plugin; + $this->Fixture->bake($className, $useTable); + } + + /** + * Assembles and writes a unit test file + * + * @param string $className Model class name + * @return string + */ + public function bakeTest($className) + { + $this->Test->interactive = $this->interactive; + $this->Test->plugin = $this->plugin; + $this->Test->connection = $this->connection; + return $this->Test->bake('Model', $className); + } + + /** + * Bake all models at once. + * + * @return void + */ + public function all() + { + $this->listAll($this->connection, false); + $unitTestExists = $this->_checkUnitTest(); + foreach ($this->_tables as $table) { + if (in_array($table, $this->skipTables)) { + continue; + } + $modelClass = Inflector::classify($table); + $this->out(__d('cake_console', 'Baking %s', $modelClass)); + $object = $this->_getModelObject($modelClass, $table); + if ($this->bake($object, false) && $unitTestExists) { + $this->bakeFixture($modelClass, $table); + $this->bakeTest($modelClass); + } + } + } + + /** + * Gets the option parser instance and configures it. + * + * @return ConsoleOptionParser + */ + public function getOptionParser() + { + $parser = parent::getOptionParser(); + + $parser->description( + __d('cake_console', 'Bake models.') + )->addArgument('name', [ + 'help' => __d('cake_console', 'Name of the model to bake. Can use Plugin.name to bake plugin models.') + ])->addSubcommand('all', [ + 'help' => __d('cake_console', 'Bake all model files with associations and validation.') + ])->addOption('plugin', [ + 'short' => 'p', + 'help' => __d('cake_console', 'Plugin to bake the model into.') + ])->addOption('theme', [ + 'short' => 't', + 'help' => __d('cake_console', 'Theme to use when baking code.') + ])->addOption('connection', [ + 'short' => 'c', + 'help' => __d('cake_console', 'The connection the model table is on.') + ])->addOption('force', [ + 'short' => 'f', + 'help' => __d('cake_console', 'Force overwriting existing files without prompting.') + ])->epilog( + __d('cake_console', 'Omitting all arguments and options will enter into an interactive mode.') + ); + + return $parser; + } } diff --git a/lib/Cake/Console/Command/Task/PluginTask.php b/lib/Cake/Console/Command/Task/PluginTask.php index 10a00864..c7d2c0b2 100755 --- a/lib/Cake/Console/Command/Task/PluginTask.php +++ b/lib/Cake/Console/Command/Task/PluginTask.php @@ -22,215 +22,223 @@ * * @package Cake.Console.Command.Task */ -class PluginTask extends AppShell { - -/** - * path to plugins directory - * - * @var array - */ - public $path = null; - -/** - * Path to the bootstrap file. Changed in tests. - * - * @var string - */ - public $bootstrap = null; - -/** - * initialize - * - * @return void - */ - public function initialize() { - $this->path = current(App::path('plugins')); - $this->bootstrap = CONFIG . 'bootstrap.php'; - } - -/** - * Execution method always used for tasks - * - * @return void - */ - public function execute() { - if (isset($this->args[0])) { - $plugin = Inflector::camelize($this->args[0]); - $pluginPath = $this->_pluginPath($plugin); - if (is_dir($pluginPath)) { - $this->out(__d('cake_console', 'Plugin: %s already exists, no action taken', $plugin)); - $this->out(__d('cake_console', 'Path: %s', $pluginPath)); - return false; - } - $this->_interactive($plugin); - } else { - return $this->_interactive(); - } - } - -/** - * Interactive interface - * - * @param string $plugin The plugin name. - * @return void - */ - protected function _interactive($plugin = null) { - while ($plugin === null) { - $plugin = $this->in(__d('cake_console', 'Enter the name of the plugin in CamelCase format')); - } - - if (!$this->bake($plugin)) { - $this->error(__d('cake_console', "An error occurred trying to bake: %s in %s", $plugin, $this->path . $plugin)); - } - } - -/** - * Bake the plugin, create directories and files - * - * @param string $plugin Name of the plugin in CamelCased format - * @return bool - */ - public function bake($plugin) { - $pathOptions = App::path('plugins'); - if (count($pathOptions) > 1) { - $this->findPath($pathOptions); - } - $this->hr(); - $this->out(__d('cake_console', "Plugin Name: %s", $plugin)); - $this->out(__d('cake_console', "Plugin Directory: %s", $this->path . $plugin)); - $this->hr(); - - $looksGood = $this->in(__d('cake_console', 'Look okay?'), array('y', 'n', 'q'), 'y'); - - if (strtolower($looksGood) === 'y') { - $Folder = new Folder($this->path . $plugin); - $directories = array( - 'Config' . DS . 'Schema', - 'Console' . DS . 'Command' . DS . 'Task', - 'Console' . DS . 'Templates', - 'Controller' . DS . 'Component', - 'Lib', - 'Locale' . DS . 'eng' . DS . 'LC_MESSAGES', - 'Model' . DS . 'Behavior', - 'Model' . DS . 'Datasource', - 'Test' . DS . 'Case' . DS . 'Controller' . DS . 'Component', - 'Test' . DS . 'Case' . DS . 'Lib', - 'Test' . DS . 'Case' . DS . 'Model' . DS . 'Behavior', - 'Test' . DS . 'Case' . DS . 'Model' . DS . 'Datasource', - 'Test' . DS . 'Case' . DS . 'View' . DS . 'Helper', - 'Test' . DS . 'Fixture', - 'View' . DS . 'Elements', - 'View' . DS . 'Helper', - 'View' . DS . 'Layouts', - 'webroot' . DS . 'css', - 'webroot' . DS . 'js', - 'webroot' . DS . 'img', - ); - - foreach ($directories as $directory) { - $dirPath = $this->path . $plugin . DS . $directory; - $Folder->create($dirPath); - new File($dirPath . DS . 'empty', true); - } - - foreach ($Folder->messages() as $message) { - $this->out($message, 1, Shell::VERBOSE); - } - - $errors = $Folder->errors(); - if (!empty($errors)) { - foreach ($errors as $message) { - $this->error($message); - } - return false; - } - - $controllerFileName = $plugin . 'AppController.php'; - - $out = "createFile($this->path . $plugin . DS . 'Controller' . DS . $controllerFileName, $out); - - $modelFileName = $plugin . 'AppModel.php'; - - $out = "createFile($this->path . $plugin . DS . 'Model' . DS . $modelFileName, $out); - - $this->_modifyBootstrap($plugin); - - $this->hr(); - $this->out(__d('cake_console', 'Created: %s in %s', $plugin, $this->path . $plugin), 2); - } - - return true; - } - -/** - * Update the app's bootstrap.php file. - * - * @param string $plugin Name of plugin - * @return void - */ - protected function _modifyBootstrap($plugin) { - $bootstrap = new File($this->bootstrap, false); - $contents = $bootstrap->read(); - if (!preg_match("@\n\s*CakePlugin::loadAll@", $contents)) { - $bootstrap->append("\nCakePlugin::load('$plugin', array('bootstrap' => false, 'routes' => false));\n"); - $this->out(''); - $this->out(__d('cake_dev', '%s modified', $this->bootstrap)); - } - } - -/** - * find and change $this->path to the user selection - * - * @param array $pathOptions The list of paths to look in. - * @return void - */ - public function findPath($pathOptions) { - $valid = false; - foreach ($pathOptions as $i => $path) { - if (!is_dir($path)) { - unset($pathOptions[$i]); - } - } - $pathOptions = array_values($pathOptions); - - $max = count($pathOptions); - while (!$valid) { - foreach ($pathOptions as $i => $option) { - $this->out($i + 1 . '. ' . $option); - } - $prompt = __d('cake_console', 'Choose a plugin path from the paths above.'); - $choice = $this->in($prompt, null, 1); - if ((int)$choice > 0 && (int)$choice <= $max) { - $valid = true; - } - } - $this->path = $pathOptions[$choice - 1]; - } - -/** - * Gets the option parser instance and configures it. - * - * @return ConsoleOptionParser - */ - public function getOptionParser() { - $parser = parent::getOptionParser(); - - $parser->description( - __d('cake_console', 'Create the directory structure, AppModel and AppController classes for a new plugin. ' . - 'Can create plugins in any of your bootstrapped plugin paths.') - )->addArgument('name', array( - 'help' => __d('cake_console', 'CamelCased name of the plugin to create.') - )); - - return $parser; - } +class PluginTask extends AppShell +{ + + /** + * path to plugins directory + * + * @var array + */ + public $path = null; + + /** + * Path to the bootstrap file. Changed in tests. + * + * @var string + */ + public $bootstrap = null; + + /** + * initialize + * + * @return void + */ + public function initialize() + { + $this->path = current(App::path('plugins')); + $this->bootstrap = CONFIG . 'bootstrap.php'; + } + + /** + * Execution method always used for tasks + * + * @return void + */ + public function execute() + { + if (isset($this->args[0])) { + $plugin = Inflector::camelize($this->args[0]); + $pluginPath = $this->_pluginPath($plugin); + if (is_dir($pluginPath)) { + $this->out(__d('cake_console', 'Plugin: %s already exists, no action taken', $plugin)); + $this->out(__d('cake_console', 'Path: %s', $pluginPath)); + return false; + } + $this->_interactive($plugin); + } else { + return $this->_interactive(); + } + } + + /** + * Interactive interface + * + * @param string $plugin The plugin name. + * @return void + */ + protected function _interactive($plugin = null) + { + while ($plugin === null) { + $plugin = $this->in(__d('cake_console', 'Enter the name of the plugin in CamelCase format')); + } + + if (!$this->bake($plugin)) { + $this->error(__d('cake_console', "An error occurred trying to bake: %s in %s", $plugin, $this->path . $plugin)); + } + } + + /** + * Bake the plugin, create directories and files + * + * @param string $plugin Name of the plugin in CamelCased format + * @return bool + */ + public function bake($plugin) + { + $pathOptions = App::path('plugins'); + if (count($pathOptions) > 1) { + $this->findPath($pathOptions); + } + $this->hr(); + $this->out(__d('cake_console', "Plugin Name: %s", $plugin)); + $this->out(__d('cake_console', "Plugin Directory: %s", $this->path . $plugin)); + $this->hr(); + + $looksGood = $this->in(__d('cake_console', 'Look okay?'), ['y', 'n', 'q'], 'y'); + + if (strtolower($looksGood) === 'y') { + $Folder = new Folder($this->path . $plugin); + $directories = [ + 'Config' . DS . 'Schema', + 'Console' . DS . 'Command' . DS . 'Task', + 'Console' . DS . 'Templates', + 'Controller' . DS . 'Component', + 'Lib', + 'Locale' . DS . 'eng' . DS . 'LC_MESSAGES', + 'Model' . DS . 'Behavior', + 'Model' . DS . 'Datasource', + 'Test' . DS . 'Case' . DS . 'Controller' . DS . 'Component', + 'Test' . DS . 'Case' . DS . 'Lib', + 'Test' . DS . 'Case' . DS . 'Model' . DS . 'Behavior', + 'Test' . DS . 'Case' . DS . 'Model' . DS . 'Datasource', + 'Test' . DS . 'Case' . DS . 'View' . DS . 'Helper', + 'Test' . DS . 'Fixture', + 'View' . DS . 'Elements', + 'View' . DS . 'Helper', + 'View' . DS . 'Layouts', + 'webroot' . DS . 'css', + 'webroot' . DS . 'js', + 'webroot' . DS . 'img', + ]; + + foreach ($directories as $directory) { + $dirPath = $this->path . $plugin . DS . $directory; + $Folder->create($dirPath); + new File($dirPath . DS . 'empty', true); + } + + foreach ($Folder->messages() as $message) { + $this->out($message, 1, Shell::VERBOSE); + } + + $errors = $Folder->errors(); + if (!empty($errors)) { + foreach ($errors as $message) { + $this->error($message); + } + return false; + } + + $controllerFileName = $plugin . 'AppController.php'; + + $out = "createFile($this->path . $plugin . DS . 'Controller' . DS . $controllerFileName, $out); + + $modelFileName = $plugin . 'AppModel.php'; + + $out = "createFile($this->path . $plugin . DS . 'Model' . DS . $modelFileName, $out); + + $this->_modifyBootstrap($plugin); + + $this->hr(); + $this->out(__d('cake_console', 'Created: %s in %s', $plugin, $this->path . $plugin), 2); + } + + return true; + } + + /** + * find and change $this->path to the user selection + * + * @param array $pathOptions The list of paths to look in. + * @return void + */ + public function findPath($pathOptions) + { + $valid = false; + foreach ($pathOptions as $i => $path) { + if (!is_dir($path)) { + unset($pathOptions[$i]); + } + } + $pathOptions = array_values($pathOptions); + + $max = count($pathOptions); + while (!$valid) { + foreach ($pathOptions as $i => $option) { + $this->out($i + 1 . '. ' . $option); + } + $prompt = __d('cake_console', 'Choose a plugin path from the paths above.'); + $choice = $this->in($prompt, null, 1); + if ((int)$choice > 0 && (int)$choice <= $max) { + $valid = true; + } + } + $this->path = $pathOptions[$choice - 1]; + } + + /** + * Update the app's bootstrap.php file. + * + * @param string $plugin Name of plugin + * @return void + */ + protected function _modifyBootstrap($plugin) + { + $bootstrap = new File($this->bootstrap, false); + $contents = $bootstrap->read(); + if (!preg_match("@\n\s*CakePlugin::loadAll@", $contents)) { + $bootstrap->append("\nCakePlugin::load('$plugin', array('bootstrap' => false, 'routes' => false));\n"); + $this->out(''); + $this->out(__d('cake_dev', '%s modified', $this->bootstrap)); + } + } + + /** + * Gets the option parser instance and configures it. + * + * @return ConsoleOptionParser + */ + public function getOptionParser() + { + $parser = parent::getOptionParser(); + + $parser->description( + __d('cake_console', 'Create the directory structure, AppModel and AppController classes for a new plugin. ' . + 'Can create plugins in any of your bootstrapped plugin paths.') + )->addArgument('name', [ + 'help' => __d('cake_console', 'CamelCased name of the plugin to create.') + ]); + + return $parser; + } } diff --git a/lib/Cake/Console/Command/Task/ProjectTask.php b/lib/Cake/Console/Command/Task/ProjectTask.php index ed38f685..b5f63eb1 100755 --- a/lib/Cake/Console/Command/Task/ProjectTask.php +++ b/lib/Cake/Console/Command/Task/ProjectTask.php @@ -26,428 +26,441 @@ * * @package Cake.Console.Command.Task */ -class ProjectTask extends AppShell { - -/** - * configs path (used in testing). - * - * @var string - */ - public $configPath = null; - -/** - * Checks that given project path does not already exist, and - * finds the app directory in it. Then it calls bake() with that information. - * - * @return mixed - */ - public function execute() { - $project = null; - if (isset($this->args[0])) { - $project = $this->args[0]; - } else { - $appContents = array_diff(scandir(APP), array('.', '..')); - if (empty($appContents)) { - $suggestedPath = rtrim(APP, DS); - } else { - $suggestedPath = APP . 'myapp'; - } - } - - while (!$project) { - $prompt = __d('cake_console', "What is the path to the project you want to bake?"); - $project = $this->in($prompt, null, $suggestedPath); - } - - if ($project && !Folder::isAbsolute($project) && isset($_SERVER['PWD'])) { - $project = $_SERVER['PWD'] . DS . $project; - } - - $response = false; - while (!$response && is_dir($project) === true && file_exists($project . 'Config' . 'core.php')) { - $prompt = __d('cake_console', 'A project already exists in this location: %s Overwrite?', $project); - $response = $this->in($prompt, array('y', 'n'), 'n'); - if (strtolower($response) === 'n') { - $response = $project = false; - } - } - - $success = true; - if ($this->bake($project)) { - $path = Folder::slashTerm($project); - - if ($this->securitySalt($path) === true) { - $this->out(__d('cake_console', ' * Random hash key created for \'Security.salt\'')); - } else { - $this->err(__d('cake_console', 'Unable to generate random hash for \'Security.salt\', you should change it in %s', CONFIG . 'core.php')); - $success = false; - } - - if ($this->securityCipherSeed($path) === true) { - $this->out(__d('cake_console', ' * Random seed created for \'Security.cipherSeed\'')); - } else { - $this->err(__d('cake_console', 'Unable to generate random seed for \'Security.cipherSeed\', you should change it in %s', CONFIG . 'core.php')); - $success = false; - } - - if ($this->cachePrefix($path)) { - $this->out(__d('cake_console', ' * Cache prefix set')); - } else { - $this->err(__d('cake_console', 'The cache prefix was NOT set')); - $success = false; - } - - if ($this->consolePath($path) === true) { - $this->out(__d('cake_console', ' * app/Console/cake.php path set.')); - } else { - $this->err(__d('cake_console', 'Unable to set console path for app/Console.')); - $success = false; - } - - $hardCode = false; - if ($this->cakeOnIncludePath()) { - $this->out(__d('cake_console', 'CakePHP is on your `include_path`. CAKE_CORE_INCLUDE_PATH will be set, but commented out.')); - } else { - $this->out(__d('cake_console', 'CakePHP is not on your `include_path`, CAKE_CORE_INCLUDE_PATH will be hard coded.')); - $this->out(__d('cake_console', 'You can fix this by adding CakePHP to your `include_path`.')); - $hardCode = true; - } - $success = $this->corePath($path, $hardCode) === true; - if ($success) { - $this->out(__d('cake_console', ' * CAKE_CORE_INCLUDE_PATH set to %s in %s', CAKE_CORE_INCLUDE_PATH, 'webroot/index.php')); - $this->out(__d('cake_console', ' * CAKE_CORE_INCLUDE_PATH set to %s in %s', CAKE_CORE_INCLUDE_PATH, 'webroot/test.php')); - } else { - $this->err(__d('cake_console', 'Unable to set CAKE_CORE_INCLUDE_PATH, you should change it in %s', $path . 'webroot' . DS . 'index.php')); - $success = false; - } - if ($success && $hardCode) { - $this->out(__d('cake_console', ' * Remember to check these values after moving to production server')); - } - - $Folder = new Folder($path); - if (!$Folder->chmod($path . 'tmp', 0777)) { - $this->err(__d('cake_console', 'Could not set permissions on %s', $path . DS . 'tmp')); - $this->out('chmod -R 0777 ' . $path . DS . 'tmp'); - $success = false; - } - if ($success) { - $this->out(__d('cake_console', 'Project baked successfully!')); - } else { - $this->out(__d('cake_console', 'Project baked but with some issues..')); - } - return $path; - } - } - -/** - * Checks PHP's include_path for CakePHP. - * - * @return bool Indicates whether or not CakePHP exists on include_path - */ - public function cakeOnIncludePath() { - $paths = explode(PATH_SEPARATOR, ini_get('include_path')); - foreach ($paths as $path) { - if (file_exists($path . DS . 'Cake' . DS . 'bootstrap.php')) { - return true; - } - } - return false; - } - -/** - * Looks for a skeleton template of a Cake application, - * and if not found asks the user for a path. When there is a path - * this method will make a deep copy of the skeleton to the project directory. - * - * @param string $path Project path - * @param string $skel Path to copy from - * @param string $skip array of directories to skip when copying - * @return mixed - */ - public function bake($path, $skel = null, $skip = array('empty')) { - if (!$skel && !empty($this->params['skel'])) { - $skel = $this->params['skel']; - } - while (!$skel) { - $skel = $this->in( - __d('cake_console', "What is the path to the directory layout you wish to copy?"), - null, - CAKE . 'Console' . DS . 'Templates' . DS . 'skel' - ); - if (!$skel) { - $this->err(__d('cake_console', 'The directory path you supplied was empty. Please try again.')); - } else { - while (is_dir($skel) === false) { - $skel = $this->in( - __d('cake_console', 'Directory path does not exist please choose another:'), - null, - CAKE . 'Console' . DS . 'Templates' . DS . 'skel' - ); - } - } - } - - $app = basename($path); - - $this->out(__d('cake_console', 'Skel Directory: ') . $skel); - $this->out(__d('cake_console', 'Will be copied to: ') . $path); - $this->hr(); - - $looksGood = $this->in(__d('cake_console', 'Look okay?'), array('y', 'n', 'q'), 'y'); - - switch (strtolower($looksGood)) { - case 'y': - $Folder = new Folder($skel); - if (!empty($this->params['empty'])) { - $skip = array(); - } - - if ($Folder->copy(array('to' => $path, 'skip' => $skip))) { - $this->hr(); - $this->out(__d('cake_console', 'Created: %s in %s', $app, $path)); - $this->hr(); - } else { - $this->err(__d('cake_console', "Could not create '%s' properly.", $app)); - return false; - } - - foreach ($Folder->messages() as $message) { - $this->out(CakeText::wrap(' * ' . $message), 1, Shell::VERBOSE); - } - - return true; - case 'n': - unset($this->args[0]); - $this->execute(); - return false; - case 'q': - $this->out(__d('cake_console', 'Bake Aborted.')); - return false; - } - } - -/** - * Generates the correct path to the CakePHP libs that are generating the project - * and points app/console/cake.php to the right place - * - * @param string $path Project path. - * @return bool success - */ - public function consolePath($path) { - $File = new File($path . 'Console' . DS . 'cake.php'); - $contents = $File->read(); - if (preg_match('/(__CAKE_PATH__)/', $contents, $match)) { - $root = strpos(CAKE_CORE_INCLUDE_PATH, '/') === 0 ? " DS . '" : "'"; - $replacement = $root . str_replace(DS, "' . DS . '", trim(CAKE_CORE_INCLUDE_PATH, DS)) . "'"; - $result = str_replace($match[0], $replacement, $contents); - if ($File->write($result)) { - return true; - } - return false; - } - return false; - } - -/** - * Generates and writes 'Security.salt' - * - * @param string $path Project path - * @return bool Success - */ - public function securitySalt($path) { - $File = new File($path . 'Config' . DS . 'core.php'); - $contents = $File->read(); - if (preg_match('/([\s]*Configure::write\(\'Security.salt\',[\s\'A-z0-9]*\);)/', $contents, $match)) { - $string = Security::generateAuthKey(); - $result = str_replace($match[0], "\t" . 'Configure::write(\'Security.salt\', \'' . $string . '\');', $contents); - if ($File->write($result)) { - return true; - } - return false; - } - return false; - } - -/** - * Generates and writes 'Security.cipherSeed' - * - * @param string $path Project path - * @return bool Success - */ - public function securityCipherSeed($path) { - $File = new File($path . 'Config' . DS . 'core.php'); - $contents = $File->read(); - if (preg_match('/([\s]*Configure::write\(\'Security.cipherSeed\',[\s\'A-z0-9]*\);)/', $contents, $match)) { - App::uses('Security', 'Utility'); - $string = substr(bin2hex(Security::generateAuthKey()), 0, 30); - $result = str_replace($match[0], "\t" . 'Configure::write(\'Security.cipherSeed\', \'' . $string . '\');', $contents); - if ($File->write($result)) { - return true; - } - return false; - } - return false; - } - -/** - * Writes cache prefix using app's name - * - * @param string $dir Path to project - * @return bool Success - */ - public function cachePrefix($dir) { - $app = basename($dir); - $File = new File($dir . 'Config' . DS . 'core.php'); - $contents = $File->read(); - if (preg_match('/(\$prefix = \'myapp_\';)/', $contents, $match)) { - $result = str_replace($match[0], '$prefix = \'' . $app . '_\';', $contents); - return $File->write($result); - } - return false; - } - -/** - * Generates and writes CAKE_CORE_INCLUDE_PATH - * - * @param string $path Project path - * @param bool $hardCode Whether or not define calls should be hardcoded. - * @return bool Success - */ - public function corePath($path, $hardCode = true) { - if (dirname($path) !== CAKE_CORE_INCLUDE_PATH) { - $filename = $path . 'webroot' . DS . 'index.php'; - if (!$this->_replaceCorePath($filename, $hardCode)) { - return false; - } - $filename = $path . 'webroot' . DS . 'test.php'; - if (!$this->_replaceCorePath($filename, $hardCode)) { - return false; - } - return true; - } - } - -/** - * Replaces the __CAKE_PATH__ placeholder in the template files. - * - * @param string $filename The filename to operate on. - * @param bool $hardCode Whether or not the define should be uncommented. - * @return bool Success - */ - protected function _replaceCorePath($filename, $hardCode) { - $contents = file_get_contents($filename); - - $root = strpos(CAKE_CORE_INCLUDE_PATH, '/') === 0 ? " DS . '" : "'"; - $corePath = $root . str_replace(DS, "' . DS . '", trim(CAKE_CORE_INCLUDE_PATH, DS)) . "'"; - - $composer = ROOT . DS . APP_DIR . DS . 'Vendor' . DS . 'cakephp' . DS . 'cakephp' . DS . 'lib'; - if (file_exists($composer)) { - $corePath = " ROOT . DS . APP_DIR . DS . 'Vendor' . DS . 'cakephp' . DS . 'cakephp' . DS . 'lib'"; - } - - $result = str_replace('__CAKE_PATH__', $corePath, $contents, $count); - if ($hardCode) { - $result = str_replace('//define(\'CAKE_CORE', 'define(\'CAKE_CORE', $result); - } - if (!file_put_contents($filename, $result)) { - return false; - } - return (bool)$count; - } - -/** - * Enables Configure::read('Routing.prefixes') in /app/Config/core.php - * - * @param string $name Name to use as admin routing - * @return bool Success - */ - public function cakeAdmin($name) { - $path = (empty($this->configPath)) ? CONFIG : $this->configPath; - $File = new File($path . 'core.php'); - $contents = $File->read(); - if (preg_match('%(\s*[/]*Configure::write\(\'Routing.prefixes\',[\s\'a-z,\)\(]*\);)%', $contents, $match)) { - $result = str_replace($match[0], "\n" . 'Configure::write(\'Routing.prefixes\', array(\'' . $name . '\'));', $contents); - if ($File->write($result)) { - Configure::write('Routing.prefixes', array($name)); - return true; - } - } - return false; - } - -/** - * Checks for Configure::read('Routing.prefixes') and forces user to input it if not enabled - * - * @return string Admin route to use - */ - public function getPrefix() { - $admin = ''; - $prefixes = Configure::read('Routing.prefixes'); - if (!empty($prefixes)) { - if (count($prefixes) === 1) { - return $prefixes[0] . '_'; - } - if ($this->interactive) { - $this->out(); - $this->out(__d('cake_console', 'You have more than one routing prefix configured')); - } - $options = array(); - foreach ($prefixes as $i => $prefix) { - $options[] = $i + 1; - if ($this->interactive) { - $this->out($i + 1 . '. ' . $prefix); - } - } - $selection = $this->in(__d('cake_console', 'Please choose a prefix to bake with.'), $options, 1); - return $prefixes[$selection - 1] . '_'; - } - if ($this->interactive) { - $this->hr(); - $this->out(__d('cake_console', 'You need to enable %s in %s to use prefix routing.', - 'Configure::write(\'Routing.prefixes\', array(\'admin\'))', - '/app/Config/core.php')); - $this->out(__d('cake_console', 'What would you like the prefix route to be?')); - $this->out(__d('cake_console', 'Example: %s', 'www.example.com/admin/controller')); - while (!$admin) { - $admin = $this->in(__d('cake_console', 'Enter a routing prefix:'), null, 'admin'); - } - if ($this->cakeAdmin($admin) !== true) { - $this->out(__d('cake_console', 'Unable to write to %s.', '/app/Config/core.php')); - $this->out(__d('cake_console', 'You need to enable %s in %s to use prefix routing.', - 'Configure::write(\'Routing.prefixes\', array(\'admin\'))', - '/app/Config/core.php')); - return $this->_stop(); - } - return $admin . '_'; - } - return ''; - } - -/** - * Gets the option parser instance and configures it. - * - * @return ConsoleOptionParser - */ - public function getOptionParser() { - $parser = parent::getOptionParser(); - - $parser->description( - __d('cake_console', 'Generate a new CakePHP project skeleton.') - )->addArgument('name', array( - 'help' => __d('cake_console', 'Application directory to make, if it starts with "/" the path is absolute.') - ))->addOption('empty', array( - 'boolean' => true, - 'help' => __d('cake_console', 'Create empty files in each of the directories. Good if you are using git') - ))->addOption('theme', array( - 'short' => 't', - 'help' => __d('cake_console', 'Theme to use when baking code.') - ))->addOption('skel', array( - 'default' => current(App::core('Console')) . 'Templates' . DS . 'skel', - 'help' => __d('cake_console', 'The directory layout to use for the new application skeleton.' . - ' Defaults to cake/Console/Templates/skel of CakePHP used to create the project.') - )); - - return $parser; - } +class ProjectTask extends AppShell +{ + + /** + * configs path (used in testing). + * + * @var string + */ + public $configPath = null; + + /** + * Checks that given project path does not already exist, and + * finds the app directory in it. Then it calls bake() with that information. + * + * @return mixed + */ + public function execute() + { + $project = null; + if (isset($this->args[0])) { + $project = $this->args[0]; + } else { + $appContents = array_diff(scandir(APP), ['.', '..']); + if (empty($appContents)) { + $suggestedPath = rtrim(APP, DS); + } else { + $suggestedPath = APP . 'myapp'; + } + } + + while (!$project) { + $prompt = __d('cake_console', "What is the path to the project you want to bake?"); + $project = $this->in($prompt, null, $suggestedPath); + } + + if ($project && !Folder::isAbsolute($project) && isset($_SERVER['PWD'])) { + $project = $_SERVER['PWD'] . DS . $project; + } + + $response = false; + while (!$response && is_dir($project) === true && file_exists($project . 'Config' . 'core.php')) { + $prompt = __d('cake_console', 'A project already exists in this location: %s Overwrite?', $project); + $response = $this->in($prompt, ['y', 'n'], 'n'); + if (strtolower($response) === 'n') { + $response = $project = false; + } + } + + $success = true; + if ($this->bake($project)) { + $path = Folder::slashTerm($project); + + if ($this->securitySalt($path) === true) { + $this->out(__d('cake_console', ' * Random hash key created for \'Security.salt\'')); + } else { + $this->err(__d('cake_console', 'Unable to generate random hash for \'Security.salt\', you should change it in %s', CONFIG . 'core.php')); + $success = false; + } + + if ($this->securityCipherSeed($path) === true) { + $this->out(__d('cake_console', ' * Random seed created for \'Security.cipherSeed\'')); + } else { + $this->err(__d('cake_console', 'Unable to generate random seed for \'Security.cipherSeed\', you should change it in %s', CONFIG . 'core.php')); + $success = false; + } + + if ($this->cachePrefix($path)) { + $this->out(__d('cake_console', ' * Cache prefix set')); + } else { + $this->err(__d('cake_console', 'The cache prefix was NOT set')); + $success = false; + } + + if ($this->consolePath($path) === true) { + $this->out(__d('cake_console', ' * app/Console/cake.php path set.')); + } else { + $this->err(__d('cake_console', 'Unable to set console path for app/Console.')); + $success = false; + } + + $hardCode = false; + if ($this->cakeOnIncludePath()) { + $this->out(__d('cake_console', 'CakePHP is on your `include_path`. CAKE_CORE_INCLUDE_PATH will be set, but commented out.')); + } else { + $this->out(__d('cake_console', 'CakePHP is not on your `include_path`, CAKE_CORE_INCLUDE_PATH will be hard coded.')); + $this->out(__d('cake_console', 'You can fix this by adding CakePHP to your `include_path`.')); + $hardCode = true; + } + $success = $this->corePath($path, $hardCode) === true; + if ($success) { + $this->out(__d('cake_console', ' * CAKE_CORE_INCLUDE_PATH set to %s in %s', CAKE_CORE_INCLUDE_PATH, 'webroot/index.php')); + $this->out(__d('cake_console', ' * CAKE_CORE_INCLUDE_PATH set to %s in %s', CAKE_CORE_INCLUDE_PATH, 'webroot/test.php')); + } else { + $this->err(__d('cake_console', 'Unable to set CAKE_CORE_INCLUDE_PATH, you should change it in %s', $path . 'webroot' . DS . 'index.php')); + $success = false; + } + if ($success && $hardCode) { + $this->out(__d('cake_console', ' * Remember to check these values after moving to production server')); + } + + $Folder = new Folder($path); + if (!$Folder->chmod($path . 'tmp', 0777)) { + $this->err(__d('cake_console', 'Could not set permissions on %s', $path . DS . 'tmp')); + $this->out('chmod -R 0777 ' . $path . DS . 'tmp'); + $success = false; + } + if ($success) { + $this->out(__d('cake_console', 'Project baked successfully!')); + } else { + $this->out(__d('cake_console', 'Project baked but with some issues..')); + } + return $path; + } + } + + /** + * Looks for a skeleton template of a Cake application, + * and if not found asks the user for a path. When there is a path + * this method will make a deep copy of the skeleton to the project directory. + * + * @param string $path Project path + * @param string $skel Path to copy from + * @param string $skip array of directories to skip when copying + * @return mixed + */ + public function bake($path, $skel = null, $skip = ['empty']) + { + if (!$skel && !empty($this->params['skel'])) { + $skel = $this->params['skel']; + } + while (!$skel) { + $skel = $this->in( + __d('cake_console', "What is the path to the directory layout you wish to copy?"), + null, + CAKE . 'Console' . DS . 'Templates' . DS . 'skel' + ); + if (!$skel) { + $this->err(__d('cake_console', 'The directory path you supplied was empty. Please try again.')); + } else { + while (is_dir($skel) === false) { + $skel = $this->in( + __d('cake_console', 'Directory path does not exist please choose another:'), + null, + CAKE . 'Console' . DS . 'Templates' . DS . 'skel' + ); + } + } + } + + $app = basename($path); + + $this->out(__d('cake_console', 'Skel Directory: ') . $skel); + $this->out(__d('cake_console', 'Will be copied to: ') . $path); + $this->hr(); + + $looksGood = $this->in(__d('cake_console', 'Look okay?'), ['y', 'n', 'q'], 'y'); + + switch (strtolower($looksGood)) { + case 'y': + $Folder = new Folder($skel); + if (!empty($this->params['empty'])) { + $skip = []; + } + + if ($Folder->copy(['to' => $path, 'skip' => $skip])) { + $this->hr(); + $this->out(__d('cake_console', 'Created: %s in %s', $app, $path)); + $this->hr(); + } else { + $this->err(__d('cake_console', "Could not create '%s' properly.", $app)); + return false; + } + + foreach ($Folder->messages() as $message) { + $this->out(CakeText::wrap(' * ' . $message), 1, Shell::VERBOSE); + } + + return true; + case 'n': + unset($this->args[0]); + $this->execute(); + return false; + case 'q': + $this->out(__d('cake_console', 'Bake Aborted.')); + return false; + } + } + + /** + * Generates and writes 'Security.salt' + * + * @param string $path Project path + * @return bool Success + */ + public function securitySalt($path) + { + $File = new File($path . 'Config' . DS . 'core.php'); + $contents = $File->read(); + if (preg_match('/([\s]*Configure::write\(\'Security.salt\',[\s\'A-z0-9]*\);)/', $contents, $match)) { + $string = Security::generateAuthKey(); + $result = str_replace($match[0], "\t" . 'Configure::write(\'Security.salt\', \'' . $string . '\');', $contents); + if ($File->write($result)) { + return true; + } + return false; + } + return false; + } + + /** + * Generates and writes 'Security.cipherSeed' + * + * @param string $path Project path + * @return bool Success + */ + public function securityCipherSeed($path) + { + $File = new File($path . 'Config' . DS . 'core.php'); + $contents = $File->read(); + if (preg_match('/([\s]*Configure::write\(\'Security.cipherSeed\',[\s\'A-z0-9]*\);)/', $contents, $match)) { + App::uses('Security', 'Utility'); + $string = substr(bin2hex(Security::generateAuthKey()), 0, 30); + $result = str_replace($match[0], "\t" . 'Configure::write(\'Security.cipherSeed\', \'' . $string . '\');', $contents); + if ($File->write($result)) { + return true; + } + return false; + } + return false; + } + + /** + * Writes cache prefix using app's name + * + * @param string $dir Path to project + * @return bool Success + */ + public function cachePrefix($dir) + { + $app = basename($dir); + $File = new File($dir . 'Config' . DS . 'core.php'); + $contents = $File->read(); + if (preg_match('/(\$prefix = \'myapp_\';)/', $contents, $match)) { + $result = str_replace($match[0], '$prefix = \'' . $app . '_\';', $contents); + return $File->write($result); + } + return false; + } + + /** + * Generates the correct path to the CakePHP libs that are generating the project + * and points app/console/cake.php to the right place + * + * @param string $path Project path. + * @return bool success + */ + public function consolePath($path) + { + $File = new File($path . 'Console' . DS . 'cake.php'); + $contents = $File->read(); + if (preg_match('/(__CAKE_PATH__)/', $contents, $match)) { + $root = strpos(CAKE_CORE_INCLUDE_PATH, '/') === 0 ? " DS . '" : "'"; + $replacement = $root . str_replace(DS, "' . DS . '", trim(CAKE_CORE_INCLUDE_PATH, DS)) . "'"; + $result = str_replace($match[0], $replacement, $contents); + if ($File->write($result)) { + return true; + } + return false; + } + return false; + } + + /** + * Checks PHP's include_path for CakePHP. + * + * @return bool Indicates whether or not CakePHP exists on include_path + */ + public function cakeOnIncludePath() + { + $paths = explode(PATH_SEPARATOR, ini_get('include_path')); + foreach ($paths as $path) { + if (file_exists($path . DS . 'Cake' . DS . 'bootstrap.php')) { + return true; + } + } + return false; + } + + /** + * Generates and writes CAKE_CORE_INCLUDE_PATH + * + * @param string $path Project path + * @param bool $hardCode Whether or not define calls should be hardcoded. + * @return bool Success + */ + public function corePath($path, $hardCode = true) + { + if (dirname($path) !== CAKE_CORE_INCLUDE_PATH) { + $filename = $path . 'webroot' . DS . 'index.php'; + if (!$this->_replaceCorePath($filename, $hardCode)) { + return false; + } + $filename = $path . 'webroot' . DS . 'test.php'; + if (!$this->_replaceCorePath($filename, $hardCode)) { + return false; + } + return true; + } + } + + /** + * Replaces the __CAKE_PATH__ placeholder in the template files. + * + * @param string $filename The filename to operate on. + * @param bool $hardCode Whether or not the define should be uncommented. + * @return bool Success + */ + protected function _replaceCorePath($filename, $hardCode) + { + $contents = file_get_contents($filename); + + $root = strpos(CAKE_CORE_INCLUDE_PATH, '/') === 0 ? " DS . '" : "'"; + $corePath = $root . str_replace(DS, "' . DS . '", trim(CAKE_CORE_INCLUDE_PATH, DS)) . "'"; + + $composer = ROOT . DS . APP_DIR . DS . 'Vendor' . DS . 'cakephp' . DS . 'cakephp' . DS . 'lib'; + if (file_exists($composer)) { + $corePath = " ROOT . DS . APP_DIR . DS . 'Vendor' . DS . 'cakephp' . DS . 'cakephp' . DS . 'lib'"; + } + + $result = str_replace('__CAKE_PATH__', $corePath, $contents, $count); + if ($hardCode) { + $result = str_replace('//define(\'CAKE_CORE', 'define(\'CAKE_CORE', $result); + } + if (!file_put_contents($filename, $result)) { + return false; + } + return (bool)$count; + } + + /** + * Checks for Configure::read('Routing.prefixes') and forces user to input it if not enabled + * + * @return string Admin route to use + */ + public function getPrefix() + { + $admin = ''; + $prefixes = Configure::read('Routing.prefixes'); + if (!empty($prefixes)) { + if (count($prefixes) === 1) { + return $prefixes[0] . '_'; + } + if ($this->interactive) { + $this->out(); + $this->out(__d('cake_console', 'You have more than one routing prefix configured')); + } + $options = []; + foreach ($prefixes as $i => $prefix) { + $options[] = $i + 1; + if ($this->interactive) { + $this->out($i + 1 . '. ' . $prefix); + } + } + $selection = $this->in(__d('cake_console', 'Please choose a prefix to bake with.'), $options, 1); + return $prefixes[$selection - 1] . '_'; + } + if ($this->interactive) { + $this->hr(); + $this->out(__d('cake_console', 'You need to enable %s in %s to use prefix routing.', + 'Configure::write(\'Routing.prefixes\', array(\'admin\'))', + '/app/Config/core.php')); + $this->out(__d('cake_console', 'What would you like the prefix route to be?')); + $this->out(__d('cake_console', 'Example: %s', 'www.example.com/admin/controller')); + while (!$admin) { + $admin = $this->in(__d('cake_console', 'Enter a routing prefix:'), null, 'admin'); + } + if ($this->cakeAdmin($admin) !== true) { + $this->out(__d('cake_console', 'Unable to write to %s.', '/app/Config/core.php')); + $this->out(__d('cake_console', 'You need to enable %s in %s to use prefix routing.', + 'Configure::write(\'Routing.prefixes\', array(\'admin\'))', + '/app/Config/core.php')); + return $this->_stop(); + } + return $admin . '_'; + } + return ''; + } + + /** + * Enables Configure::read('Routing.prefixes') in /app/Config/core.php + * + * @param string $name Name to use as admin routing + * @return bool Success + */ + public function cakeAdmin($name) + { + $path = (empty($this->configPath)) ? CONFIG : $this->configPath; + $File = new File($path . 'core.php'); + $contents = $File->read(); + if (preg_match('%(\s*[/]*Configure::write\(\'Routing.prefixes\',[\s\'a-z,\)\(]*\);)%', $contents, $match)) { + $result = str_replace($match[0], "\n" . 'Configure::write(\'Routing.prefixes\', array(\'' . $name . '\'));', $contents); + if ($File->write($result)) { + Configure::write('Routing.prefixes', [$name]); + return true; + } + } + return false; + } + + /** + * Gets the option parser instance and configures it. + * + * @return ConsoleOptionParser + */ + public function getOptionParser() + { + $parser = parent::getOptionParser(); + + $parser->description( + __d('cake_console', 'Generate a new CakePHP project skeleton.') + )->addArgument('name', [ + 'help' => __d('cake_console', 'Application directory to make, if it starts with "/" the path is absolute.') + ])->addOption('empty', [ + 'boolean' => true, + 'help' => __d('cake_console', 'Create empty files in each of the directories. Good if you are using git') + ])->addOption('theme', [ + 'short' => 't', + 'help' => __d('cake_console', 'Theme to use when baking code.') + ])->addOption('skel', [ + 'default' => current(App::core('Console')) . 'Templates' . DS . 'skel', + 'help' => __d('cake_console', 'The directory layout to use for the new application skeleton.' . + ' Defaults to cake/Console/Templates/skel of CakePHP used to create the project.') + ]); + + return $parser; + } } diff --git a/lib/Cake/Console/Command/Task/TemplateTask.php b/lib/Cake/Console/Command/Task/TemplateTask.php index 0691bf5c..a09d57c8 100755 --- a/lib/Cake/Console/Command/Task/TemplateTask.php +++ b/lib/Cake/Console/Command/Task/TemplateTask.php @@ -24,194 +24,201 @@ * * @package Cake.Console.Command.Task */ -class TemplateTask extends AppShell { - -/** - * variables to add to template scope - * - * @var array - */ - public $templateVars = array(); - -/** - * Paths to look for templates on. - * Contains a list of $theme => $path - * - * @var array - */ - public $templatePaths = array(); - -/** - * Initialize callback. Setup paths for the template task. - * - * @return void - */ - public function initialize() { - $this->templatePaths = $this->_findThemes(); - } - -/** - * Find the paths to all the installed shell themes in the app. - * - * Bake themes are directories not named `skel` inside a `Console/Templates` path. - * They are listed in this order: app -> plugin -> default - * - * @return array Array of bake themes that are installed. - */ - protected function _findThemes() { - $paths = App::path('Console'); - - $plugins = App::objects('plugin'); - foreach ($plugins as $plugin) { - $paths[] = $this->_pluginPath($plugin) . 'Console' . DS; - } - - $core = current(App::core('Console')); - $separator = DS === '/' ? '/' : '\\\\'; - $core = preg_replace('#shells' . $separator . '$#', '', $core); - - $Folder = new Folder($core . 'Templates' . DS . 'default'); - - $contents = $Folder->read(); - $themeFolders = $contents[0]; - - $paths[] = $core; - - foreach ($paths as $i => $path) { - $paths[$i] = rtrim($path, DS) . DS; - } - - $themes = array(); - foreach ($paths as $path) { - $Folder = new Folder($path . 'Templates', false); - $contents = $Folder->read(); - $subDirs = $contents[0]; - foreach ($subDirs as $dir) { - if (empty($dir) || preg_match('@^skel$|_skel$@', $dir)) { - continue; - } - $Folder = new Folder($path . 'Templates' . DS . $dir); - $contents = $Folder->read(); - $subDirs = $contents[0]; - if (array_intersect($contents[0], $themeFolders)) { - $templateDir = $path . 'Templates' . DS . $dir . DS; - $themes[$dir] = $templateDir; - } - } - } - return $themes; - } - -/** - * Set variable values to the template scope - * - * @param string|array $one A string or an array of data. - * @param string|array $two Value in case $one is a string (which then works as the key). - * Unused if $one is an associative array, otherwise serves as the values to $one's keys. - * @return void - */ - public function set($one, $two = null) { - if (is_array($one)) { - if (is_array($two)) { - $data = array_combine($one, $two); - } else { - $data = $one; - } - } else { - $data = array($one => $two); - } - - if (!$data) { - return false; - } - $this->templateVars = $data + $this->templateVars; - } - -/** - * Runs the template - * - * @param string $directory directory / type of thing you want - * @param string $filename template name - * @param array $vars Additional vars to set to template scope. - * @return string contents of generated code template - */ - public function generate($directory, $filename, $vars = null) { - if ($vars !== null) { - $this->set($vars); - } - if (empty($this->templatePaths)) { - $this->initialize(); - } - $themePath = $this->getThemePath(); - $templateFile = $this->_findTemplate($themePath, $directory, $filename); - if ($templateFile) { - extract($this->templateVars); - ob_start(); - ob_implicit_flush(0); - include $templateFile; - $content = ob_get_clean(); - return $content; - } - return ''; - } - -/** - * Find the theme name for the current operation. - * If there is only one theme in $templatePaths it will be used. - * If there is a -theme param in the cli args, it will be used. - * If there is more than one installed theme user interaction will happen - * - * @return string returns the path to the selected theme. - */ - public function getThemePath() { - if (count($this->templatePaths) === 1) { - $paths = array_values($this->templatePaths); - return $paths[0]; - } - if (!empty($this->params['theme']) && isset($this->templatePaths[$this->params['theme']])) { - return $this->templatePaths[$this->params['theme']]; - } - - $this->hr(); - $this->out(__d('cake_console', 'You have more than one set of templates installed.')); - $this->out(__d('cake_console', 'Please choose the template set you wish to use:')); - $this->hr(); - - $i = 1; - $indexedPaths = array(); - foreach ($this->templatePaths as $key => $path) { - $this->out($i . '. ' . $key); - $indexedPaths[$i] = $path; - $i++; - } - $index = $this->in(__d('cake_console', 'Which bake theme would you like to use?'), range(1, $i - 1), 1); - $themeNames = array_keys($this->templatePaths); - $this->params['theme'] = $themeNames[$index - 1]; - return $indexedPaths[$index]; - } - -/** - * Find a template inside a directory inside a path. - * Will scan all other theme dirs if the template is not found in the first directory. - * - * @param string $path The initial path to look for the file on. If it is not found fallbacks will be used. - * @param string $directory Subdirectory to look for ie. 'views', 'objects' - * @param string $filename lower_case_underscored filename you want. - * @return string filename will exit program if template is not found. - */ - protected function _findTemplate($path, $directory, $filename) { - $themeFile = $path . $directory . DS . $filename . '.ctp'; - if (file_exists($themeFile)) { - return $themeFile; - } - foreach ($this->templatePaths as $path) { - $templatePath = $path . $directory . DS . $filename . '.ctp'; - if (file_exists($templatePath)) { - return $templatePath; - } - } - $this->err(__d('cake_console', 'Could not find template for %s', $filename)); - return false; - } +class TemplateTask extends AppShell +{ + + /** + * variables to add to template scope + * + * @var array + */ + public $templateVars = []; + + /** + * Paths to look for templates on. + * Contains a list of $theme => $path + * + * @var array + */ + public $templatePaths = []; + + /** + * Runs the template + * + * @param string $directory directory / type of thing you want + * @param string $filename template name + * @param array $vars Additional vars to set to template scope. + * @return string contents of generated code template + */ + public function generate($directory, $filename, $vars = null) + { + if ($vars !== null) { + $this->set($vars); + } + if (empty($this->templatePaths)) { + $this->initialize(); + } + $themePath = $this->getThemePath(); + $templateFile = $this->_findTemplate($themePath, $directory, $filename); + if ($templateFile) { + extract($this->templateVars); + ob_start(); + ob_implicit_flush(0); + include $templateFile; + $content = ob_get_clean(); + return $content; + } + return ''; + } + + /** + * Set variable values to the template scope + * + * @param string|array $one A string or an array of data. + * @param string|array $two Value in case $one is a string (which then works as the key). + * Unused if $one is an associative array, otherwise serves as the values to $one's keys. + * @return void + */ + public function set($one, $two = null) + { + if (is_array($one)) { + if (is_array($two)) { + $data = array_combine($one, $two); + } else { + $data = $one; + } + } else { + $data = [$one => $two]; + } + + if (!$data) { + return false; + } + $this->templateVars = $data + $this->templateVars; + } + + /** + * Initialize callback. Setup paths for the template task. + * + * @return void + */ + public function initialize() + { + $this->templatePaths = $this->_findThemes(); + } + + /** + * Find the paths to all the installed shell themes in the app. + * + * Bake themes are directories not named `skel` inside a `Console/Templates` path. + * They are listed in this order: app -> plugin -> default + * + * @return array Array of bake themes that are installed. + */ + protected function _findThemes() + { + $paths = App::path('Console'); + + $plugins = App::objects('plugin'); + foreach ($plugins as $plugin) { + $paths[] = $this->_pluginPath($plugin) . 'Console' . DS; + } + + $core = current(App::core('Console')); + $separator = DS === '/' ? '/' : '\\\\'; + $core = preg_replace('#shells' . $separator . '$#', '', $core); + + $Folder = new Folder($core . 'Templates' . DS . 'default'); + + $contents = $Folder->read(); + $themeFolders = $contents[0]; + + $paths[] = $core; + + foreach ($paths as $i => $path) { + $paths[$i] = rtrim($path, DS) . DS; + } + + $themes = []; + foreach ($paths as $path) { + $Folder = new Folder($path . 'Templates', false); + $contents = $Folder->read(); + $subDirs = $contents[0]; + foreach ($subDirs as $dir) { + if (empty($dir) || preg_match('@^skel$|_skel$@', $dir)) { + continue; + } + $Folder = new Folder($path . 'Templates' . DS . $dir); + $contents = $Folder->read(); + $subDirs = $contents[0]; + if (array_intersect($contents[0], $themeFolders)) { + $templateDir = $path . 'Templates' . DS . $dir . DS; + $themes[$dir] = $templateDir; + } + } + } + return $themes; + } + + /** + * Find the theme name for the current operation. + * If there is only one theme in $templatePaths it will be used. + * If there is a -theme param in the cli args, it will be used. + * If there is more than one installed theme user interaction will happen + * + * @return string returns the path to the selected theme. + */ + public function getThemePath() + { + if (count($this->templatePaths) === 1) { + $paths = array_values($this->templatePaths); + return $paths[0]; + } + if (!empty($this->params['theme']) && isset($this->templatePaths[$this->params['theme']])) { + return $this->templatePaths[$this->params['theme']]; + } + + $this->hr(); + $this->out(__d('cake_console', 'You have more than one set of templates installed.')); + $this->out(__d('cake_console', 'Please choose the template set you wish to use:')); + $this->hr(); + + $i = 1; + $indexedPaths = []; + foreach ($this->templatePaths as $key => $path) { + $this->out($i . '. ' . $key); + $indexedPaths[$i] = $path; + $i++; + } + $index = $this->in(__d('cake_console', 'Which bake theme would you like to use?'), range(1, $i - 1), 1); + $themeNames = array_keys($this->templatePaths); + $this->params['theme'] = $themeNames[$index - 1]; + return $indexedPaths[$index]; + } + + /** + * Find a template inside a directory inside a path. + * Will scan all other theme dirs if the template is not found in the first directory. + * + * @param string $path The initial path to look for the file on. If it is not found fallbacks will be used. + * @param string $directory Subdirectory to look for ie. 'views', 'objects' + * @param string $filename lower_case_underscored filename you want. + * @return string filename will exit program if template is not found. + */ + protected function _findTemplate($path, $directory, $filename) + { + $themeFile = $path . $directory . DS . $filename . '.ctp'; + if (file_exists($themeFile)) { + return $themeFile; + } + foreach ($this->templatePaths as $path) { + $templatePath = $path . $directory . DS . $filename . '.ctp'; + if (file_exists($templatePath)) { + return $templatePath; + } + } + $this->err(__d('cake_console', 'Could not find template for %s', $filename)); + return false; + } } diff --git a/lib/Cake/Console/Command/Task/TestTask.php b/lib/Cake/Console/Command/Task/TestTask.php index 342494e5..5631b4f3 100755 --- a/lib/Cake/Console/Command/Task/TestTask.php +++ b/lib/Cake/Console/Command/Task/TestTask.php @@ -24,559 +24,582 @@ * * @package Cake.Console.Command.Task */ -class TestTask extends BakeTask { - -/** - * path to TESTS directory - * - * @var string - */ - public $path = TESTS; - -/** - * Tasks used. - * - * @var array - */ - public $tasks = array('Template'); - -/** - * class types that methods can be generated for - * - * @var array - */ - public $classTypes = array( - 'Model' => 'Model', - 'Controller' => 'Controller', - 'Component' => 'Controller/Component', - 'Behavior' => 'Model/Behavior', - 'Helper' => 'View/Helper' - ); - -/** - * Mapping between packages, and their baseclass + package. - * This is used to generate App::uses() call to autoload base - * classes if a developer has forgotten to do so. - * - * @var array - */ - public $baseTypes = array( - 'Model' => array('Model', 'Model'), - 'Behavior' => array('ModelBehavior', 'Model'), - 'Controller' => array('Controller', 'Controller'), - 'Component' => array('Component', 'Controller'), - 'Helper' => array('Helper', 'View') - ); - -/** - * Internal list of fixtures that have been added so far. - * - * @var array - */ - protected $_fixtures = array(); - -/** - * Execution method always used for tasks - * - * @return void - */ - public function execute() { - parent::execute(); - $count = count($this->args); - if (!$count) { - $this->_interactive(); - } - - if ($count === 1) { - $this->_interactive($this->args[0]); - } - - if ($count > 1) { - $type = Inflector::classify($this->args[0]); - if ($this->bake($type, $this->args[1])) { - $this->out('Done'); - } - } - } - -/** - * Handles interactive baking - * - * @param string $type The type of object to bake a test for. - * @return string|bool - */ - protected function _interactive($type = null) { - $this->interactive = true; - $this->hr(); - $this->out(__d('cake_console', 'Bake Tests')); - $this->out(__d('cake_console', 'Path: %s', $this->getPath())); - $this->hr(); - - if ($type) { - $type = Inflector::camelize($type); - if (!isset($this->classTypes[$type])) { - $this->error(__d('cake_console', 'Incorrect type provided. Please choose one of %s', implode(', ', array_keys($this->classTypes)))); - } - } else { - $type = $this->getObjectType(); - } - $className = $this->getClassName($type); - return $this->bake($type, $className); - } - -/** - * Completes final steps for generating data to create test case. - * - * @param string $type Type of object to bake test case for ie. Model, Controller - * @param string $className the 'cake name' for the class ie. Posts for the PostsController - * @return string|bool - */ - public function bake($type, $className) { - $plugin = null; - if ($this->plugin) { - $plugin = $this->plugin . '.'; - } - - $realType = $this->mapType($type, $plugin); - $fullClassName = $this->getRealClassName($type, $className); - - if ($this->typeCanDetectFixtures($type) && $this->isLoadableClass($realType, $fullClassName)) { - $this->out(__d('cake_console', 'Bake is detecting possible fixtures...')); - $testSubject = $this->buildTestSubject($type, $className); - $this->generateFixtureList($testSubject); - } elseif ($this->interactive) { - $this->getUserFixtures(); - } - list($baseClass, $baseType) = $this->getBaseType($type); - App::uses($baseClass, $baseType); - App::uses($fullClassName, $realType); - - $methods = array(); - if (class_exists($fullClassName)) { - $methods = $this->getTestableMethods($fullClassName); - } - $mock = $this->hasMockClass($type, $fullClassName); - list($preConstruct, $construction, $postConstruct) = $this->generateConstructor($type, $fullClassName, $plugin); - $uses = $this->generateUses($type, $realType, $fullClassName); - - $this->out("\n" . __d('cake_console', 'Baking test case for %s %s ...', $className, $type), 1, Shell::QUIET); - - $this->Template->set('fixtures', $this->_fixtures); - $this->Template->set('plugin', $plugin); - $this->Template->set(compact( - 'className', 'methods', 'type', 'fullClassName', 'mock', - 'realType', 'preConstruct', 'postConstruct', 'construction', - 'uses' - )); - $out = $this->Template->generate('classes', 'test'); - - $filename = $this->testCaseFileName($type, $className); - $made = $this->createFile($filename, $out); - if ($made) { - return $out; - } - return false; - } - -/** - * Interact with the user and get their chosen type. Can exit the script. - * - * @return string Users chosen type. - */ - public function getObjectType() { - $this->hr(); - $this->out(__d('cake_console', 'Select an object type:')); - $this->hr(); - - $keys = array(); - $i = 0; - foreach ($this->classTypes as $option => $package) { - $this->out(++$i . '. ' . $option); - $keys[] = $i; - } - $keys[] = 'q'; - $selection = $this->in(__d('cake_console', 'Enter the type of object to bake a test for or (q)uit'), $keys, 'q'); - if ($selection === 'q') { - return $this->_stop(); - } - $types = array_keys($this->classTypes); - return $types[$selection - 1]; - } - -/** - * Get the user chosen Class name for the chosen type - * - * @param string $objectType Type of object to list classes for i.e. Model, Controller. - * @return string Class name the user chose. - */ - public function getClassName($objectType) { - $type = ucfirst(strtolower($objectType)); - $typeLength = strlen($type); - $type = $this->classTypes[$type]; - if ($this->plugin) { - $plugin = $this->plugin . '.'; - $options = App::objects($plugin . $type); - } else { - $options = App::objects($type); - } - $this->out(__d('cake_console', 'Choose a %s class', $objectType)); - $keys = array(); - foreach ($options as $key => $option) { - $this->out(++$key . '. ' . $option); - $keys[] = $key; - } - while (empty($selection)) { - $selection = $this->in(__d('cake_console', 'Choose an existing class, or enter the name of a class that does not exist')); - if (is_numeric($selection) && isset($options[$selection - 1])) { - $selection = $options[$selection - 1]; - } - if ($type !== 'Model') { - $selection = substr($selection, 0, $typeLength * - 1); - } - } - return $selection; - } - -/** - * Checks whether the chosen type can find its own fixtures. - * Currently only model, and controller are supported - * - * @param string $type The Type of object you are generating tests for eg. controller - * @return bool - */ - public function typeCanDetectFixtures($type) { - $type = strtolower($type); - return in_array($type, array('controller', 'model')); - } - -/** - * Check if a class with the given package is loaded or can be loaded. - * - * @param string $package The package of object you are generating tests for eg. controller - * @param string $class the Classname of the class the test is being generated for. - * @return bool - */ - public function isLoadableClass($package, $class) { - App::uses($class, $package); - list($plugin, $ns) = pluginSplit($package); - if ($plugin) { - App::uses("{$plugin}AppController", $package); - App::uses("{$plugin}AppModel", $package); - App::uses("{$plugin}AppHelper", $package); - } - return class_exists($class); - } - -/** - * Construct an instance of the class to be tested. - * So that fixtures can be detected - * - * @param string $type The Type of object you are generating tests for eg. controller - * @param string $class the Classname of the class the test is being generated for. - * @return object And instance of the class that is going to be tested. - */ - public function buildTestSubject($type, $class) { - ClassRegistry::flush(); - App::uses($class, $type); - $class = $this->getRealClassName($type, $class); - if (strtolower($type) === 'model') { - $instance = ClassRegistry::init($class); - } else { - $instance = new $class(); - } - return $instance; - } - -/** - * Gets the real class name from the cake short form. If the class name is already - * suffixed with the type, the type will not be duplicated. - * - * @param string $type The Type of object you are generating tests for eg. controller - * @param string $class the Classname of the class the test is being generated for. - * @return string Real class name - */ - public function getRealClassName($type, $class) { - if (strtolower($type) === 'model' || empty($this->classTypes[$type])) { - return $class; - } - - $position = strpos($class, $type); - - if ($position !== false && (strlen($class) - $position) === strlen($type)) { - return $class; - } - return $class . $type; - } - -/** - * Map the types that TestTask uses to concrete types that App::uses can use. - * - * @param string $type The type of thing having a test generated. - * @param string $plugin The plugin name. - * @return string - * @throws CakeException When invalid object types are requested. - */ - public function mapType($type, $plugin) { - $type = ucfirst($type); - if (empty($this->classTypes[$type])) { - throw new CakeException(__d('cake_dev', 'Invalid object type.')); - } - $real = $this->classTypes[$type]; - if ($plugin) { - $real = trim($plugin, '.') . '.' . $real; - } - return $real; - } - -/** - * Get the base class and package name for a given type. - * - * @param string $type The type the class having a test - * generated for is in. - * @return array Array of (class, type) - * @throws CakeException on invalid types. - */ - public function getBaseType($type) { - if (empty($this->baseTypes[$type])) { - throw new CakeException(__d('cake_dev', 'Invalid type name')); - } - return $this->baseTypes[$type]; - } - -/** - * Get methods declared in the class given. - * No parent methods will be returned - * - * @param string $className Name of class to look at. - * @return array Array of method names. - */ - public function getTestableMethods($className) { - $classMethods = get_class_methods($className); - $parentMethods = get_class_methods(get_parent_class($className)); - $thisMethods = array_diff($classMethods, $parentMethods); - $out = array(); - foreach ($thisMethods as $method) { - if (substr($method, 0, 1) !== '_' && $method != strtolower($className)) { - $out[] = $method; - } - } - return $out; - } - -/** - * Generate the list of fixtures that will be required to run this test based on - * loaded models. - * - * @param CakeObject $subject The object you want to generate fixtures for. - * @return array Array of fixtures to be included in the test. - */ - public function generateFixtureList($subject) { - $this->_fixtures = array(); - if ($subject instanceof Model) { - $this->_processModel($subject); - } elseif ($subject instanceof Controller) { - $this->_processController($subject); - } - return array_values($this->_fixtures); - } - -/** - * Process a model recursively and pull out all the - * model names converting them to fixture names. - * - * @param Model $subject A Model class to scan for associations and pull fixtures off of. - * @return void - */ - protected function _processModel($subject) { - $this->_addFixture($subject->name); - $associated = $subject->getAssociated(); - foreach ($associated as $alias => $type) { - $className = $subject->{$alias}->name; - if (!isset($this->_fixtures[$className])) { - $this->_processModel($subject->{$alias}); - } - if ($type === 'hasAndBelongsToMany') { - if (!empty($subject->hasAndBelongsToMany[$alias]['with'])) { - list(, $joinModel) = pluginSplit($subject->hasAndBelongsToMany[$alias]['with']); - } else { - $joinModel = Inflector::classify($subject->hasAndBelongsToMany[$alias]['joinTable']); - } - if (!isset($this->_fixtures[$joinModel])) { - $this->_processModel($subject->{$joinModel}); - } - } - } - } - -/** - * Process all the models attached to a controller - * and generate a fixture list. - * - * @param Controller $subject A controller to pull model names off of. - * @return void - */ - protected function _processController($subject) { - $subject->constructClasses(); - $models = array(Inflector::classify($subject->name)); - if (!empty($subject->uses)) { - $models = $subject->uses; - } - foreach ($models as $model) { - list(, $model) = pluginSplit($model); - $this->_processModel($subject->{$model}); - } - } - -/** - * Add class name to the fixture list. - * Sets the app. or plugin.plugin_name. prefix. - * - * @param string $name Name of the Model class that a fixture might be required for. - * @return void - */ - protected function _addFixture($name) { - if ($this->plugin) { - $prefix = 'plugin.' . Inflector::underscore($this->plugin) . '.'; - } else { - $prefix = 'app.'; - } - $fixture = $prefix . Inflector::underscore($name); - $this->_fixtures[$name] = $fixture; - } - -/** - * Interact with the user to get additional fixtures they want to use. - * - * @return array Array of fixtures the user wants to add. - */ - public function getUserFixtures() { - $proceed = $this->in(__d('cake_console', 'Bake could not detect fixtures, would you like to add some?'), array('y', 'n'), 'n'); - $fixtures = array(); - if (strtolower($proceed) === 'y') { - $fixtureList = $this->in(__d('cake_console', "Please provide a comma separated list of the fixtures names you'd like to use.\nExample: 'app.comment, app.post, plugin.forums.post'")); - $fixtureListTrimmed = str_replace(' ', '', $fixtureList); - $fixtures = explode(',', $fixtureListTrimmed); - } - $this->_fixtures = array_merge($this->_fixtures, $fixtures); - return $fixtures; - } - -/** - * Is a mock class required for this type of test? - * Controllers require a mock class. - * - * @param string $type The type of object tests are being generated for eg. controller. - * @return bool - */ - public function hasMockClass($type) { - $type = strtolower($type); - return $type === 'controller'; - } - -/** - * Generate a constructor code snippet for the type and class name - * - * @param string $type The Type of object you are generating tests for eg. controller - * @param string $fullClassName The Classname of the class the test is being generated for. - * @param string $plugin The plugin name. - * @return array Constructor snippets for the thing you are building. - */ - public function generateConstructor($type, $fullClassName, $plugin) { - $type = strtolower($type); - $pre = $construct = $post = ''; - if ($type === 'model') { - $construct = "ClassRegistry::init('{$plugin}$fullClassName');\n"; - } - if ($type === 'behavior') { - $construct = "new $fullClassName();\n"; - } - if ($type === 'helper') { - $pre = "\$View = new View();\n"; - $construct = "new {$fullClassName}(\$View);\n"; - } - if ($type === 'component') { - $pre = "\$Collection = new ComponentCollection();\n"; - $construct = "new {$fullClassName}(\$Collection);\n"; - } - return array($pre, $construct, $post); - } - -/** - * Generate the uses() calls for a type & class name - * - * @param string $type The Type of object you are generating tests for eg. controller - * @param string $realType The package name for the class. - * @param string $className The Classname of the class the test is being generated for. - * @return array An array containing used classes - */ - public function generateUses($type, $realType, $className) { - $uses = array(); - $type = strtolower($type); - if ($type === 'component') { - $uses[] = array('ComponentCollection', 'Controller'); - $uses[] = array('Component', 'Controller'); - } - if ($type === 'helper') { - $uses[] = array('View', 'View'); - $uses[] = array('Helper', 'View'); - } - $uses[] = array($className, $realType); - return $uses; - } - -/** - * Make the filename for the test case. resolve the suffixes for controllers - * and get the plugin path if needed. - * - * @param string $type The Type of object you are generating tests for eg. controller - * @param string $className the Classname of the class the test is being generated for. - * @return string filename the test should be created on. - */ - public function testCaseFileName($type, $className) { - $path = $this->getPath() . 'Case' . DS; - $type = Inflector::camelize($type); - if (isset($this->classTypes[$type])) { - $path .= $this->classTypes[$type] . DS; - } - $className = $this->getRealClassName($type, $className); - return str_replace('/', DS, $path) . Inflector::camelize($className) . 'Test.php'; - } - -/** - * Gets the option parser instance and configures it. - * - * @return ConsoleOptionParser - */ - public function getOptionParser() { - $parser = parent::getOptionParser(); - - $parser->description( - __d('cake_console', 'Bake test case skeletons for classes.') - )->addArgument('type', array( - 'help' => __d('cake_console', 'Type of class to bake, can be any of the following: controller, model, helper, component or behavior.'), - 'choices' => array( - 'Controller', 'controller', - 'Model', 'model', - 'Helper', 'helper', - 'Component', 'component', - 'Behavior', 'behavior' - ) - ))->addArgument('name', array( - 'help' => __d('cake_console', 'An existing class to bake tests for.') - ))->addOption('theme', array( - 'short' => 't', - 'help' => __d('cake_console', 'Theme to use when baking code.') - ))->addOption('plugin', array( - 'short' => 'p', - 'help' => __d('cake_console', 'CamelCased name of the plugin to bake tests for.') - ))->addOption('force', array( - 'short' => 'f', - 'help' => __d('cake_console', 'Force overwriting existing files without prompting.') - ))->epilog( - __d('cake_console', 'Omitting all arguments and options will enter into an interactive mode.') - ); - - return $parser; - } +class TestTask extends BakeTask +{ + + /** + * path to TESTS directory + * + * @var string + */ + public $path = TESTS; + + /** + * Tasks used. + * + * @var array + */ + public $tasks = ['Template']; + + /** + * class types that methods can be generated for + * + * @var array + */ + public $classTypes = [ + 'Model' => 'Model', + 'Controller' => 'Controller', + 'Component' => 'Controller/Component', + 'Behavior' => 'Model/Behavior', + 'Helper' => 'View/Helper' + ]; + + /** + * Mapping between packages, and their baseclass + package. + * This is used to generate App::uses() call to autoload base + * classes if a developer has forgotten to do so. + * + * @var array + */ + public $baseTypes = [ + 'Model' => ['Model', 'Model'], + 'Behavior' => ['ModelBehavior', 'Model'], + 'Controller' => ['Controller', 'Controller'], + 'Component' => ['Component', 'Controller'], + 'Helper' => ['Helper', 'View'] + ]; + + /** + * Internal list of fixtures that have been added so far. + * + * @var array + */ + protected $_fixtures = []; + + /** + * Execution method always used for tasks + * + * @return void + */ + public function execute() + { + parent::execute(); + $count = count($this->args); + if (!$count) { + $this->_interactive(); + } + + if ($count === 1) { + $this->_interactive($this->args[0]); + } + + if ($count > 1) { + $type = Inflector::classify($this->args[0]); + if ($this->bake($type, $this->args[1])) { + $this->out('Done'); + } + } + } + + /** + * Handles interactive baking + * + * @param string $type The type of object to bake a test for. + * @return string|bool + */ + protected function _interactive($type = null) + { + $this->interactive = true; + $this->hr(); + $this->out(__d('cake_console', 'Bake Tests')); + $this->out(__d('cake_console', 'Path: %s', $this->getPath())); + $this->hr(); + + if ($type) { + $type = Inflector::camelize($type); + if (!isset($this->classTypes[$type])) { + $this->error(__d('cake_console', 'Incorrect type provided. Please choose one of %s', implode(', ', array_keys($this->classTypes)))); + } + } else { + $type = $this->getObjectType(); + } + $className = $this->getClassName($type); + return $this->bake($type, $className); + } + + /** + * Interact with the user and get their chosen type. Can exit the script. + * + * @return string Users chosen type. + */ + public function getObjectType() + { + $this->hr(); + $this->out(__d('cake_console', 'Select an object type:')); + $this->hr(); + + $keys = []; + $i = 0; + foreach ($this->classTypes as $option => $package) { + $this->out(++$i . '. ' . $option); + $keys[] = $i; + } + $keys[] = 'q'; + $selection = $this->in(__d('cake_console', 'Enter the type of object to bake a test for or (q)uit'), $keys, 'q'); + if ($selection === 'q') { + return $this->_stop(); + } + $types = array_keys($this->classTypes); + return $types[$selection - 1]; + } + + /** + * Get the user chosen Class name for the chosen type + * + * @param string $objectType Type of object to list classes for i.e. Model, Controller. + * @return string Class name the user chose. + */ + public function getClassName($objectType) + { + $type = ucfirst(strtolower($objectType)); + $typeLength = strlen($type); + $type = $this->classTypes[$type]; + if ($this->plugin) { + $plugin = $this->plugin . '.'; + $options = App::objects($plugin . $type); + } else { + $options = App::objects($type); + } + $this->out(__d('cake_console', 'Choose a %s class', $objectType)); + $keys = []; + foreach ($options as $key => $option) { + $this->out(++$key . '. ' . $option); + $keys[] = $key; + } + while (empty($selection)) { + $selection = $this->in(__d('cake_console', 'Choose an existing class, or enter the name of a class that does not exist')); + if (is_numeric($selection) && isset($options[$selection - 1])) { + $selection = $options[$selection - 1]; + } + if ($type !== 'Model') { + $selection = substr($selection, 0, $typeLength * -1); + } + } + return $selection; + } + + /** + * Completes final steps for generating data to create test case. + * + * @param string $type Type of object to bake test case for ie. Model, Controller + * @param string $className the 'cake name' for the class ie. Posts for the PostsController + * @return string|bool + */ + public function bake($type, $className) + { + $plugin = null; + if ($this->plugin) { + $plugin = $this->plugin . '.'; + } + + $realType = $this->mapType($type, $plugin); + $fullClassName = $this->getRealClassName($type, $className); + + if ($this->typeCanDetectFixtures($type) && $this->isLoadableClass($realType, $fullClassName)) { + $this->out(__d('cake_console', 'Bake is detecting possible fixtures...')); + $testSubject = $this->buildTestSubject($type, $className); + $this->generateFixtureList($testSubject); + } else if ($this->interactive) { + $this->getUserFixtures(); + } + list($baseClass, $baseType) = $this->getBaseType($type); + App::uses($baseClass, $baseType); + App::uses($fullClassName, $realType); + + $methods = []; + if (class_exists($fullClassName)) { + $methods = $this->getTestableMethods($fullClassName); + } + $mock = $this->hasMockClass($type, $fullClassName); + list($preConstruct, $construction, $postConstruct) = $this->generateConstructor($type, $fullClassName, $plugin); + $uses = $this->generateUses($type, $realType, $fullClassName); + + $this->out("\n" . __d('cake_console', 'Baking test case for %s %s ...', $className, $type), 1, Shell::QUIET); + + $this->Template->set('fixtures', $this->_fixtures); + $this->Template->set('plugin', $plugin); + $this->Template->set(compact( + 'className', 'methods', 'type', 'fullClassName', 'mock', + 'realType', 'preConstruct', 'postConstruct', 'construction', + 'uses' + )); + $out = $this->Template->generate('classes', 'test'); + + $filename = $this->testCaseFileName($type, $className); + $made = $this->createFile($filename, $out); + if ($made) { + return $out; + } + return false; + } + + /** + * Map the types that TestTask uses to concrete types that App::uses can use. + * + * @param string $type The type of thing having a test generated. + * @param string $plugin The plugin name. + * @return string + * @throws CakeException When invalid object types are requested. + */ + public function mapType($type, $plugin) + { + $type = ucfirst($type); + if (empty($this->classTypes[$type])) { + throw new CakeException(__d('cake_dev', 'Invalid object type.')); + } + $real = $this->classTypes[$type]; + if ($plugin) { + $real = trim($plugin, '.') . '.' . $real; + } + return $real; + } + + /** + * Gets the real class name from the cake short form. If the class name is already + * suffixed with the type, the type will not be duplicated. + * + * @param string $type The Type of object you are generating tests for eg. controller + * @param string $class the Classname of the class the test is being generated for. + * @return string Real class name + */ + public function getRealClassName($type, $class) + { + if (strtolower($type) === 'model' || empty($this->classTypes[$type])) { + return $class; + } + + $position = strpos($class, $type); + + if ($position !== false && (strlen($class) - $position) === strlen($type)) { + return $class; + } + return $class . $type; + } + + /** + * Checks whether the chosen type can find its own fixtures. + * Currently only model, and controller are supported + * + * @param string $type The Type of object you are generating tests for eg. controller + * @return bool + */ + public function typeCanDetectFixtures($type) + { + $type = strtolower($type); + return in_array($type, ['controller', 'model']); + } + + /** + * Check if a class with the given package is loaded or can be loaded. + * + * @param string $package The package of object you are generating tests for eg. controller + * @param string $class the Classname of the class the test is being generated for. + * @return bool + */ + public function isLoadableClass($package, $class) + { + App::uses($class, $package); + list($plugin, $ns) = pluginSplit($package); + if ($plugin) { + App::uses("{$plugin}AppController", $package); + App::uses("{$plugin}AppModel", $package); + App::uses("{$plugin}AppHelper", $package); + } + return class_exists($class); + } + + /** + * Construct an instance of the class to be tested. + * So that fixtures can be detected + * + * @param string $type The Type of object you are generating tests for eg. controller + * @param string $class the Classname of the class the test is being generated for. + * @return object And instance of the class that is going to be tested. + */ + public function buildTestSubject($type, $class) + { + ClassRegistry::flush(); + App::uses($class, $type); + $class = $this->getRealClassName($type, $class); + if (strtolower($type) === 'model') { + $instance = ClassRegistry::init($class); + } else { + $instance = new $class(); + } + return $instance; + } + + /** + * Generate the list of fixtures that will be required to run this test based on + * loaded models. + * + * @param CakeObject $subject The object you want to generate fixtures for. + * @return array Array of fixtures to be included in the test. + */ + public function generateFixtureList($subject) + { + $this->_fixtures = []; + if ($subject instanceof Model) { + $this->_processModel($subject); + } else if ($subject instanceof Controller) { + $this->_processController($subject); + } + return array_values($this->_fixtures); + } + + /** + * Process a model recursively and pull out all the + * model names converting them to fixture names. + * + * @param Model $subject A Model class to scan for associations and pull fixtures off of. + * @return void + */ + protected function _processModel($subject) + { + $this->_addFixture($subject->name); + $associated = $subject->getAssociated(); + foreach ($associated as $alias => $type) { + $className = $subject->{$alias}->name; + if (!isset($this->_fixtures[$className])) { + $this->_processModel($subject->{$alias}); + } + if ($type === 'hasAndBelongsToMany') { + if (!empty($subject->hasAndBelongsToMany[$alias]['with'])) { + list(, $joinModel) = pluginSplit($subject->hasAndBelongsToMany[$alias]['with']); + } else { + $joinModel = Inflector::classify($subject->hasAndBelongsToMany[$alias]['joinTable']); + } + if (!isset($this->_fixtures[$joinModel])) { + $this->_processModel($subject->{$joinModel}); + } + } + } + } + + /** + * Add class name to the fixture list. + * Sets the app. or plugin.plugin_name. prefix. + * + * @param string $name Name of the Model class that a fixture might be required for. + * @return void + */ + protected function _addFixture($name) + { + if ($this->plugin) { + $prefix = 'plugin.' . Inflector::underscore($this->plugin) . '.'; + } else { + $prefix = 'app.'; + } + $fixture = $prefix . Inflector::underscore($name); + $this->_fixtures[$name] = $fixture; + } + + /** + * Process all the models attached to a controller + * and generate a fixture list. + * + * @param Controller $subject A controller to pull model names off of. + * @return void + */ + protected function _processController($subject) + { + $subject->constructClasses(); + $models = [Inflector::classify($subject->name)]; + if (!empty($subject->uses)) { + $models = $subject->uses; + } + foreach ($models as $model) { + list(, $model) = pluginSplit($model); + $this->_processModel($subject->{$model}); + } + } + + /** + * Interact with the user to get additional fixtures they want to use. + * + * @return array Array of fixtures the user wants to add. + */ + public function getUserFixtures() + { + $proceed = $this->in(__d('cake_console', 'Bake could not detect fixtures, would you like to add some?'), ['y', 'n'], 'n'); + $fixtures = []; + if (strtolower($proceed) === 'y') { + $fixtureList = $this->in(__d('cake_console', "Please provide a comma separated list of the fixtures names you'd like to use.\nExample: 'app.comment, app.post, plugin.forums.post'")); + $fixtureListTrimmed = str_replace(' ', '', $fixtureList); + $fixtures = explode(',', $fixtureListTrimmed); + } + $this->_fixtures = array_merge($this->_fixtures, $fixtures); + return $fixtures; + } + + /** + * Get the base class and package name for a given type. + * + * @param string $type The type the class having a test + * generated for is in. + * @return array Array of (class, type) + * @throws CakeException on invalid types. + */ + public function getBaseType($type) + { + if (empty($this->baseTypes[$type])) { + throw new CakeException(__d('cake_dev', 'Invalid type name')); + } + return $this->baseTypes[$type]; + } + + /** + * Get methods declared in the class given. + * No parent methods will be returned + * + * @param string $className Name of class to look at. + * @return array Array of method names. + */ + public function getTestableMethods($className) + { + $classMethods = get_class_methods($className); + $parentMethods = get_class_methods(get_parent_class($className)); + $thisMethods = array_diff($classMethods, $parentMethods); + $out = []; + foreach ($thisMethods as $method) { + if (substr($method, 0, 1) !== '_' && $method != strtolower($className)) { + $out[] = $method; + } + } + return $out; + } + + /** + * Is a mock class required for this type of test? + * Controllers require a mock class. + * + * @param string $type The type of object tests are being generated for eg. controller. + * @return bool + */ + public function hasMockClass($type) + { + $type = strtolower($type); + return $type === 'controller'; + } + + /** + * Generate a constructor code snippet for the type and class name + * + * @param string $type The Type of object you are generating tests for eg. controller + * @param string $fullClassName The Classname of the class the test is being generated for. + * @param string $plugin The plugin name. + * @return array Constructor snippets for the thing you are building. + */ + public function generateConstructor($type, $fullClassName, $plugin) + { + $type = strtolower($type); + $pre = $construct = $post = ''; + if ($type === 'model') { + $construct = "ClassRegistry::init('{$plugin}$fullClassName');\n"; + } + if ($type === 'behavior') { + $construct = "new $fullClassName();\n"; + } + if ($type === 'helper') { + $pre = "\$View = new View();\n"; + $construct = "new {$fullClassName}(\$View);\n"; + } + if ($type === 'component') { + $pre = "\$Collection = new ComponentCollection();\n"; + $construct = "new {$fullClassName}(\$Collection);\n"; + } + return [$pre, $construct, $post]; + } + + /** + * Generate the uses() calls for a type & class name + * + * @param string $type The Type of object you are generating tests for eg. controller + * @param string $realType The package name for the class. + * @param string $className The Classname of the class the test is being generated for. + * @return array An array containing used classes + */ + public function generateUses($type, $realType, $className) + { + $uses = []; + $type = strtolower($type); + if ($type === 'component') { + $uses[] = ['ComponentCollection', 'Controller']; + $uses[] = ['Component', 'Controller']; + } + if ($type === 'helper') { + $uses[] = ['View', 'View']; + $uses[] = ['Helper', 'View']; + } + $uses[] = [$className, $realType]; + return $uses; + } + + /** + * Make the filename for the test case. resolve the suffixes for controllers + * and get the plugin path if needed. + * + * @param string $type The Type of object you are generating tests for eg. controller + * @param string $className the Classname of the class the test is being generated for. + * @return string filename the test should be created on. + */ + public function testCaseFileName($type, $className) + { + $path = $this->getPath() . 'Case' . DS; + $type = Inflector::camelize($type); + if (isset($this->classTypes[$type])) { + $path .= $this->classTypes[$type] . DS; + } + $className = $this->getRealClassName($type, $className); + return str_replace('/', DS, $path) . Inflector::camelize($className) . 'Test.php'; + } + + /** + * Gets the option parser instance and configures it. + * + * @return ConsoleOptionParser + */ + public function getOptionParser() + { + $parser = parent::getOptionParser(); + + $parser->description( + __d('cake_console', 'Bake test case skeletons for classes.') + )->addArgument('type', [ + 'help' => __d('cake_console', 'Type of class to bake, can be any of the following: controller, model, helper, component or behavior.'), + 'choices' => [ + 'Controller', 'controller', + 'Model', 'model', + 'Helper', 'helper', + 'Component', 'component', + 'Behavior', 'behavior' + ] + ])->addArgument('name', [ + 'help' => __d('cake_console', 'An existing class to bake tests for.') + ])->addOption('theme', [ + 'short' => 't', + 'help' => __d('cake_console', 'Theme to use when baking code.') + ])->addOption('plugin', [ + 'short' => 'p', + 'help' => __d('cake_console', 'CamelCased name of the plugin to bake tests for.') + ])->addOption('force', [ + 'short' => 'f', + 'help' => __d('cake_console', 'Force overwriting existing files without prompting.') + ])->epilog( + __d('cake_console', 'Omitting all arguments and options will enter into an interactive mode.') + ); + + return $parser; + } } diff --git a/lib/Cake/Console/Command/Task/ViewTask.php b/lib/Cake/Console/Command/Task/ViewTask.php index fa7ddef4..3de1f1a3 100755 --- a/lib/Cake/Console/Command/Task/ViewTask.php +++ b/lib/Cake/Console/Command/Task/ViewTask.php @@ -24,454 +24,468 @@ * * @package Cake.Console.Command.Task */ -class ViewTask extends BakeTask { - -/** - * Tasks to be loaded by this Task - * - * @var array - */ - public $tasks = array('Project', 'Controller', 'DbConfig', 'Template'); - -/** - * path to View directory - * - * @var array - */ - public $path = null; - -/** - * Name of the controller being used - * - * @var string - */ - public $controllerName = null; - -/** - * The template file to use - * - * @var string - */ - public $template = null; - -/** - * Actions to use for scaffolding - * - * @var array - */ - public $scaffoldActions = array('index', 'view', 'add', 'edit'); - -/** - * An array of action names that don't require templates. These - * actions will not emit errors when doing bakeActions() - * - * @var array - */ - public $noTemplateActions = array('delete'); - -/** - * Override initialize - * - * @return void - */ - public function initialize() { - $this->path = current(App::path('View')); - } - -/** - * Execution method always used for tasks - * - * @return mixed - */ - public function execute() { - parent::execute(); - if (empty($this->args)) { - $this->_interactive(); - } - if (empty($this->args[0])) { - return null; - } - if (!isset($this->connection)) { - $this->connection = 'default'; - } - $action = null; - $this->controllerName = $this->_controllerName($this->args[0]); - - $this->Project->interactive = false; - if (strtolower($this->args[0]) === 'all') { - return $this->all(); - } - - if (isset($this->args[1])) { - $this->template = $this->args[1]; - } - if (isset($this->args[2])) { - $action = $this->args[2]; - } - if (!$action) { - $action = $this->template; - } - if ($action) { - return $this->bake($action, true); - } - - $vars = $this->_loadController(); - $methods = $this->_methodsToBake(); - - foreach ($methods as $method) { - $content = $this->getContent($method, $vars); - if ($content) { - $this->bake($method, $content); - } - } - } - -/** - * Get a list of actions that can / should have views baked for them. - * - * @return array Array of action names that should be baked - */ - protected function _methodsToBake() { - $methods = array_diff( - array_map('strtolower', get_class_methods($this->controllerName . 'Controller')), - array_map('strtolower', get_class_methods('AppController')) - ); - $scaffoldActions = false; - if (empty($methods)) { - $scaffoldActions = true; - $methods = $this->scaffoldActions; - } - $adminRoute = $this->Project->getPrefix(); - foreach ($methods as $i => $method) { - if ($adminRoute && !empty($this->params['admin'])) { - if ($scaffoldActions) { - $methods[$i] = $adminRoute . $method; - continue; - } elseif (strpos($method, $adminRoute) === false) { - unset($methods[$i]); - } - } - if ($method[0] === '_' || $method === strtolower($this->controllerName . 'Controller')) { - unset($methods[$i]); - } - } - return $methods; - } - -/** - * Bake All views for All controllers. - * - * @return void - */ - public function all() { - $this->Controller->interactive = false; - $tables = $this->Controller->listAll($this->connection, false); - - $actions = null; - if (isset($this->args[1])) { - $actions = array($this->args[1]); - } - $this->interactive = false; - foreach ($tables as $table) { - $model = $this->_modelName($table); - $this->controllerName = $this->_controllerName($model); - App::uses($model, 'Model'); - if (class_exists($model)) { - $vars = $this->_loadController(); - if (!$actions) { - $actions = $this->_methodsToBake(); - } - $this->bakeActions($actions, $vars); - $actions = null; - } - } - } - -/** - * Handles interactive baking - * - * @return void - */ - protected function _interactive() { - $this->hr(); - $this->out(sprintf("Bake View\nPath: %s", $this->getPath())); - $this->hr(); - - $this->DbConfig->interactive = $this->Controller->interactive = $this->interactive = true; - - if (empty($this->connection)) { - $this->connection = $this->DbConfig->getConfig(); - } - - $this->Controller->connection = $this->connection; - $this->controllerName = $this->Controller->getName(); - - $prompt = __d('cake_console', "Would you like bake to build your views interactively?\nWarning: Choosing no will overwrite %s views if they exist.", $this->controllerName); - $interactive = $this->in($prompt, array('y', 'n'), 'n'); - - if (strtolower($interactive) === 'n') { - $this->interactive = false; - } - - $prompt = __d('cake_console', "Would you like to create some CRUD views\n(index, add, view, edit) for this controller?\nNOTE: Before doing so, you'll need to create your controller\nand model classes (including associated models)."); - $wannaDoScaffold = $this->in($prompt, array('y', 'n'), 'y'); - - $wannaDoAdmin = $this->in(__d('cake_console', "Would you like to create the views for admin routing?"), array('y', 'n'), 'n'); - - if (strtolower($wannaDoScaffold) === 'y' || strtolower($wannaDoAdmin) === 'y') { - $vars = $this->_loadController(); - if (strtolower($wannaDoScaffold) === 'y') { - $actions = $this->scaffoldActions; - $this->bakeActions($actions, $vars); - } - if (strtolower($wannaDoAdmin) === 'y') { - $admin = $this->Project->getPrefix(); - $regularActions = $this->scaffoldActions; - $adminActions = array(); - foreach ($regularActions as $action) { - $adminActions[] = $admin . $action; - } - $this->bakeActions($adminActions, $vars); - } - $this->hr(); - $this->out(); - $this->out(__d('cake_console', "View Scaffolding Complete.\n")); - } else { - $this->customAction(); - } - } - -/** - * Loads Controller and sets variables for the template - * Available template variables - * 'modelClass', 'primaryKey', 'displayField', 'singularVar', 'pluralVar', - * 'singularHumanName', 'pluralHumanName', 'fields', 'foreignKeys', - * 'belongsTo', 'hasOne', 'hasMany', 'hasAndBelongsToMany' - * - * @return array Returns a variables to be made available to a view template - */ - protected function _loadController() { - if (!$this->controllerName) { - $this->err(__d('cake_console', 'Controller not found')); - } - - $plugin = null; - if ($this->plugin) { - $plugin = $this->plugin . '.'; - } - - $controllerClassName = $this->controllerName . 'Controller'; - App::uses($controllerClassName, $plugin . 'Controller'); - if (!class_exists($controllerClassName)) { - $file = $controllerClassName . '.php'; - $this->err(__d('cake_console', "The file '%s' could not be found.\nIn order to bake a view, you'll need to first create the controller.", $file)); - return $this->_stop(); - } - $controllerObj = new $controllerClassName(); - $controllerObj->plugin = $this->plugin; - $controllerObj->constructClasses(); - $modelClass = $controllerObj->modelClass; - $modelObj = $controllerObj->{$controllerObj->modelClass}; - - if ($modelObj) { - $primaryKey = $modelObj->primaryKey; - $displayField = $modelObj->displayField; - $singularVar = Inflector::variable($modelClass); - $singularHumanName = $this->_singularHumanName($this->controllerName); - $schema = $modelObj->schema(true); - $fields = array_keys($schema); - $associations = $this->_associations($modelObj); - } else { - $primaryKey = $displayField = null; - $singularVar = Inflector::variable(Inflector::singularize($this->controllerName)); - $singularHumanName = $this->_singularHumanName($this->controllerName); - $fields = $schema = $associations = array(); - } - $pluralVar = Inflector::variable($this->controllerName); - $pluralHumanName = $this->_pluralHumanName($this->controllerName); - - return compact('modelClass', 'schema', 'primaryKey', 'displayField', 'singularVar', 'pluralVar', - 'singularHumanName', 'pluralHumanName', 'fields', 'associations'); - } - -/** - * Bake a view file for each of the supplied actions - * - * @param array $actions Array of actions to make files for. - * @param array $vars The template variables. - * @return void - */ - public function bakeActions($actions, $vars) { - foreach ($actions as $action) { - $content = $this->getContent($action, $vars); - $this->bake($action, $content); - } - } - -/** - * handle creation of baking a custom action view file - * - * @return void - */ - public function customAction() { - $action = ''; - while (!$action) { - $action = $this->in(__d('cake_console', 'Action Name? (use lowercase_underscored function name)')); - if (!$action) { - $this->out(__d('cake_console', 'The action name you supplied was empty. Please try again.')); - } - } - $this->out(); - $this->hr(); - $this->out(__d('cake_console', 'The following view will be created:')); - $this->hr(); - $this->out(__d('cake_console', 'Controller Name: %s', $this->controllerName)); - $this->out(__d('cake_console', 'Action Name: %s', $action)); - $this->out(__d('cake_console', 'Path: %s', $this->getPath() . $this->controllerName . DS . Inflector::underscore($action) . ".ctp")); - $this->hr(); - $looksGood = $this->in(__d('cake_console', 'Look okay?'), array('y', 'n'), 'y'); - if (strtolower($looksGood) === 'y') { - $this->bake($action, ' '); - return $this->_stop(); - } - $this->out(__d('cake_console', 'Bake Aborted.')); - } - -/** - * Assembles and writes bakes the view file. - * - * @param string $action Action to bake - * @param string $content Content to write - * @return bool Success - */ - public function bake($action, $content = '') { - if ($content === true) { - $content = $this->getContent($action); - } - if (empty($content)) { - return false; - } - $this->out("\n" . __d('cake_console', 'Baking `%s` view file...', $action), 1, Shell::QUIET); - $path = $this->getPath(); - $filename = $path . $this->controllerName . DS . Inflector::underscore($action) . '.ctp'; - return $this->createFile($filename, $content); - } - -/** - * Builds content from template and variables - * - * @param string $action name to generate content to - * @param array $vars passed for use in templates - * @return string content from template - */ - public function getContent($action, $vars = null) { - if (!$vars) { - $vars = $this->_loadController(); - } - - $this->Template->set('action', $action); - $this->Template->set('plugin', $this->plugin); - $this->Template->set($vars); - $template = $this->getTemplate($action); - if ($template) { - return $this->Template->generate('views', $template); - } - return false; - } - -/** - * Gets the template name based on the action name - * - * @param string $action name - * @return string template name - */ - public function getTemplate($action) { - if ($action != $this->template && in_array($action, $this->noTemplateActions)) { - return false; - } - if (!empty($this->template) && $action != $this->template) { - return $this->template; - } - $themePath = $this->Template->getThemePath(); - if (file_exists($themePath . 'views' . DS . $action . '.ctp')) { - return $action; - } - $template = $action; - $prefixes = Configure::read('Routing.prefixes'); - foreach ((array)$prefixes as $prefix) { - if (strpos($template, $prefix) !== false) { - $template = str_replace($prefix . '_', '', $template); - } - } - if (in_array($template, array('add', 'edit'))) { - $template = 'form'; - } elseif (preg_match('@(_add|_edit)$@', $template)) { - $template = str_replace(array('_add', '_edit'), '_form', $template); - } - return $template; - } - -/** - * Gets the option parser instance and configures it. - * - * @return ConsoleOptionParser - */ - public function getOptionParser() { - $parser = parent::getOptionParser(); - - $parser->description( - __d('cake_console', 'Bake views for a controller, using built-in or custom templates.') - )->addArgument('controller', array( - 'help' => __d('cake_console', 'Name of the controller views to bake. Can be Plugin.name as a shortcut for plugin baking.') - ))->addArgument('action', array( - 'help' => __d('cake_console', "Will bake a single action's file. core templates are (index, add, edit, view)") - ))->addArgument('alias', array( - 'help' => __d('cake_console', 'Will bake the template in but create the filename after .') - ))->addOption('plugin', array( - 'short' => 'p', - 'help' => __d('cake_console', 'Plugin to bake the view into.') - ))->addOption('admin', array( - 'help' => __d('cake_console', 'Set to only bake views for a prefix in Routing.prefixes'), - 'boolean' => true - ))->addOption('theme', array( - 'short' => 't', - 'help' => __d('cake_console', 'Theme to use when baking code.') - ))->addOption('connection', array( - 'short' => 'c', - 'help' => __d('cake_console', 'The connection the connected model is on.') - ))->addOption('force', array( - 'short' => 'f', - 'help' => __d('cake_console', 'Force overwriting existing files without prompting.') - ))->addSubcommand('all', array( - 'help' => __d('cake_console', 'Bake all CRUD action views for all controllers. Requires models and controllers to exist.') - ))->epilog( - __d('cake_console', 'Omitting all arguments and options will enter into an interactive mode.') - ); - - return $parser; - } - -/** - * Returns associations for controllers models. - * - * @param Model $model The Model instance. - * @return array associations - */ - protected function _associations(Model $model) { - $keys = array('belongsTo', 'hasOne', 'hasMany', 'hasAndBelongsToMany'); - $associations = array(); - - foreach ($keys as $type) { - foreach ($model->{$type} as $assocKey => $assocData) { - list(, $modelClass) = pluginSplit($assocData['className']); - $associations[$type][$assocKey]['primaryKey'] = $model->{$assocKey}->primaryKey; - $associations[$type][$assocKey]['displayField'] = $model->{$assocKey}->displayField; - $associations[$type][$assocKey]['foreignKey'] = $assocData['foreignKey']; - $associations[$type][$assocKey]['controller'] = Inflector::pluralize(Inflector::underscore($modelClass)); - $associations[$type][$assocKey]['fields'] = array_keys($model->{$assocKey}->schema(true)); - } - } - return $associations; - } +class ViewTask extends BakeTask +{ + + /** + * Tasks to be loaded by this Task + * + * @var array + */ + public $tasks = ['Project', 'Controller', 'DbConfig', 'Template']; + + /** + * path to View directory + * + * @var array + */ + public $path = null; + + /** + * Name of the controller being used + * + * @var string + */ + public $controllerName = null; + + /** + * The template file to use + * + * @var string + */ + public $template = null; + + /** + * Actions to use for scaffolding + * + * @var array + */ + public $scaffoldActions = ['index', 'view', 'add', 'edit']; + + /** + * An array of action names that don't require templates. These + * actions will not emit errors when doing bakeActions() + * + * @var array + */ + public $noTemplateActions = ['delete']; + + /** + * Override initialize + * + * @return void + */ + public function initialize() + { + $this->path = current(App::path('View')); + } + + /** + * Execution method always used for tasks + * + * @return mixed + */ + public function execute() + { + parent::execute(); + if (empty($this->args)) { + $this->_interactive(); + } + if (empty($this->args[0])) { + return null; + } + if (!isset($this->connection)) { + $this->connection = 'default'; + } + $action = null; + $this->controllerName = $this->_controllerName($this->args[0]); + + $this->Project->interactive = false; + if (strtolower($this->args[0]) === 'all') { + return $this->all(); + } + + if (isset($this->args[1])) { + $this->template = $this->args[1]; + } + if (isset($this->args[2])) { + $action = $this->args[2]; + } + if (!$action) { + $action = $this->template; + } + if ($action) { + return $this->bake($action, true); + } + + $vars = $this->_loadController(); + $methods = $this->_methodsToBake(); + + foreach ($methods as $method) { + $content = $this->getContent($method, $vars); + if ($content) { + $this->bake($method, $content); + } + } + } + + /** + * Handles interactive baking + * + * @return void + */ + protected function _interactive() + { + $this->hr(); + $this->out(sprintf("Bake View\nPath: %s", $this->getPath())); + $this->hr(); + + $this->DbConfig->interactive = $this->Controller->interactive = $this->interactive = true; + + if (empty($this->connection)) { + $this->connection = $this->DbConfig->getConfig(); + } + + $this->Controller->connection = $this->connection; + $this->controllerName = $this->Controller->getName(); + + $prompt = __d('cake_console', "Would you like bake to build your views interactively?\nWarning: Choosing no will overwrite %s views if they exist.", $this->controllerName); + $interactive = $this->in($prompt, ['y', 'n'], 'n'); + + if (strtolower($interactive) === 'n') { + $this->interactive = false; + } + + $prompt = __d('cake_console', "Would you like to create some CRUD views\n(index, add, view, edit) for this controller?\nNOTE: Before doing so, you'll need to create your controller\nand model classes (including associated models)."); + $wannaDoScaffold = $this->in($prompt, ['y', 'n'], 'y'); + + $wannaDoAdmin = $this->in(__d('cake_console', "Would you like to create the views for admin routing?"), ['y', 'n'], 'n'); + + if (strtolower($wannaDoScaffold) === 'y' || strtolower($wannaDoAdmin) === 'y') { + $vars = $this->_loadController(); + if (strtolower($wannaDoScaffold) === 'y') { + $actions = $this->scaffoldActions; + $this->bakeActions($actions, $vars); + } + if (strtolower($wannaDoAdmin) === 'y') { + $admin = $this->Project->getPrefix(); + $regularActions = $this->scaffoldActions; + $adminActions = []; + foreach ($regularActions as $action) { + $adminActions[] = $admin . $action; + } + $this->bakeActions($adminActions, $vars); + } + $this->hr(); + $this->out(); + $this->out(__d('cake_console', "View Scaffolding Complete.\n")); + } else { + $this->customAction(); + } + } + + /** + * Loads Controller and sets variables for the template + * Available template variables + * 'modelClass', 'primaryKey', 'displayField', 'singularVar', 'pluralVar', + * 'singularHumanName', 'pluralHumanName', 'fields', 'foreignKeys', + * 'belongsTo', 'hasOne', 'hasMany', 'hasAndBelongsToMany' + * + * @return array Returns a variables to be made available to a view template + */ + protected function _loadController() + { + if (!$this->controllerName) { + $this->err(__d('cake_console', 'Controller not found')); + } + + $plugin = null; + if ($this->plugin) { + $plugin = $this->plugin . '.'; + } + + $controllerClassName = $this->controllerName . 'Controller'; + App::uses($controllerClassName, $plugin . 'Controller'); + if (!class_exists($controllerClassName)) { + $file = $controllerClassName . '.php'; + $this->err(__d('cake_console', "The file '%s' could not be found.\nIn order to bake a view, you'll need to first create the controller.", $file)); + return $this->_stop(); + } + $controllerObj = new $controllerClassName(); + $controllerObj->plugin = $this->plugin; + $controllerObj->constructClasses(); + $modelClass = $controllerObj->modelClass; + $modelObj = $controllerObj->{$controllerObj->modelClass}; + + if ($modelObj) { + $primaryKey = $modelObj->primaryKey; + $displayField = $modelObj->displayField; + $singularVar = Inflector::variable($modelClass); + $singularHumanName = $this->_singularHumanName($this->controllerName); + $schema = $modelObj->schema(true); + $fields = array_keys($schema); + $associations = $this->_associations($modelObj); + } else { + $primaryKey = $displayField = null; + $singularVar = Inflector::variable(Inflector::singularize($this->controllerName)); + $singularHumanName = $this->_singularHumanName($this->controllerName); + $fields = $schema = $associations = []; + } + $pluralVar = Inflector::variable($this->controllerName); + $pluralHumanName = $this->_pluralHumanName($this->controllerName); + + return compact('modelClass', 'schema', 'primaryKey', 'displayField', 'singularVar', 'pluralVar', + 'singularHumanName', 'pluralHumanName', 'fields', 'associations'); + } + + /** + * Returns associations for controllers models. + * + * @param Model $model The Model instance. + * @return array associations + */ + protected function _associations(Model $model) + { + $keys = ['belongsTo', 'hasOne', 'hasMany', 'hasAndBelongsToMany']; + $associations = []; + + foreach ($keys as $type) { + foreach ($model->{$type} as $assocKey => $assocData) { + list(, $modelClass) = pluginSplit($assocData['className']); + $associations[$type][$assocKey]['primaryKey'] = $model->{$assocKey}->primaryKey; + $associations[$type][$assocKey]['displayField'] = $model->{$assocKey}->displayField; + $associations[$type][$assocKey]['foreignKey'] = $assocData['foreignKey']; + $associations[$type][$assocKey]['controller'] = Inflector::pluralize(Inflector::underscore($modelClass)); + $associations[$type][$assocKey]['fields'] = array_keys($model->{$assocKey}->schema(true)); + } + } + return $associations; + } + + /** + * Bake a view file for each of the supplied actions + * + * @param array $actions Array of actions to make files for. + * @param array $vars The template variables. + * @return void + */ + public function bakeActions($actions, $vars) + { + foreach ($actions as $action) { + $content = $this->getContent($action, $vars); + $this->bake($action, $content); + } + } + + /** + * Builds content from template and variables + * + * @param string $action name to generate content to + * @param array $vars passed for use in templates + * @return string content from template + */ + public function getContent($action, $vars = null) + { + if (!$vars) { + $vars = $this->_loadController(); + } + + $this->Template->set('action', $action); + $this->Template->set('plugin', $this->plugin); + $this->Template->set($vars); + $template = $this->getTemplate($action); + if ($template) { + return $this->Template->generate('views', $template); + } + return false; + } + + /** + * Gets the template name based on the action name + * + * @param string $action name + * @return string template name + */ + public function getTemplate($action) + { + if ($action != $this->template && in_array($action, $this->noTemplateActions)) { + return false; + } + if (!empty($this->template) && $action != $this->template) { + return $this->template; + } + $themePath = $this->Template->getThemePath(); + if (file_exists($themePath . 'views' . DS . $action . '.ctp')) { + return $action; + } + $template = $action; + $prefixes = Configure::read('Routing.prefixes'); + foreach ((array)$prefixes as $prefix) { + if (strpos($template, $prefix) !== false) { + $template = str_replace($prefix . '_', '', $template); + } + } + if (in_array($template, ['add', 'edit'])) { + $template = 'form'; + } else if (preg_match('@(_add|_edit)$@', $template)) { + $template = str_replace(['_add', '_edit'], '_form', $template); + } + return $template; + } + + /** + * Assembles and writes bakes the view file. + * + * @param string $action Action to bake + * @param string $content Content to write + * @return bool Success + */ + public function bake($action, $content = '') + { + if ($content === true) { + $content = $this->getContent($action); + } + if (empty($content)) { + return false; + } + $this->out("\n" . __d('cake_console', 'Baking `%s` view file...', $action), 1, Shell::QUIET); + $path = $this->getPath(); + $filename = $path . $this->controllerName . DS . Inflector::underscore($action) . '.ctp'; + return $this->createFile($filename, $content); + } + + /** + * handle creation of baking a custom action view file + * + * @return void + */ + public function customAction() + { + $action = ''; + while (!$action) { + $action = $this->in(__d('cake_console', 'Action Name? (use lowercase_underscored function name)')); + if (!$action) { + $this->out(__d('cake_console', 'The action name you supplied was empty. Please try again.')); + } + } + $this->out(); + $this->hr(); + $this->out(__d('cake_console', 'The following view will be created:')); + $this->hr(); + $this->out(__d('cake_console', 'Controller Name: %s', $this->controllerName)); + $this->out(__d('cake_console', 'Action Name: %s', $action)); + $this->out(__d('cake_console', 'Path: %s', $this->getPath() . $this->controllerName . DS . Inflector::underscore($action) . ".ctp")); + $this->hr(); + $looksGood = $this->in(__d('cake_console', 'Look okay?'), ['y', 'n'], 'y'); + if (strtolower($looksGood) === 'y') { + $this->bake($action, ' '); + return $this->_stop(); + } + $this->out(__d('cake_console', 'Bake Aborted.')); + } + + /** + * Bake All views for All controllers. + * + * @return void + */ + public function all() + { + $this->Controller->interactive = false; + $tables = $this->Controller->listAll($this->connection, false); + + $actions = null; + if (isset($this->args[1])) { + $actions = [$this->args[1]]; + } + $this->interactive = false; + foreach ($tables as $table) { + $model = $this->_modelName($table); + $this->controllerName = $this->_controllerName($model); + App::uses($model, 'Model'); + if (class_exists($model)) { + $vars = $this->_loadController(); + if (!$actions) { + $actions = $this->_methodsToBake(); + } + $this->bakeActions($actions, $vars); + $actions = null; + } + } + } + + /** + * Get a list of actions that can / should have views baked for them. + * + * @return array Array of action names that should be baked + */ + protected function _methodsToBake() + { + $methods = array_diff( + array_map('strtolower', get_class_methods($this->controllerName . 'Controller')), + array_map('strtolower', get_class_methods('AppController')) + ); + $scaffoldActions = false; + if (empty($methods)) { + $scaffoldActions = true; + $methods = $this->scaffoldActions; + } + $adminRoute = $this->Project->getPrefix(); + foreach ($methods as $i => $method) { + if ($adminRoute && !empty($this->params['admin'])) { + if ($scaffoldActions) { + $methods[$i] = $adminRoute . $method; + continue; + } else if (strpos($method, $adminRoute) === false) { + unset($methods[$i]); + } + } + if ($method[0] === '_' || $method === strtolower($this->controllerName . 'Controller')) { + unset($methods[$i]); + } + } + return $methods; + } + + /** + * Gets the option parser instance and configures it. + * + * @return ConsoleOptionParser + */ + public function getOptionParser() + { + $parser = parent::getOptionParser(); + + $parser->description( + __d('cake_console', 'Bake views for a controller, using built-in or custom templates.') + )->addArgument('controller', [ + 'help' => __d('cake_console', 'Name of the controller views to bake. Can be Plugin.name as a shortcut for plugin baking.') + ])->addArgument('action', [ + 'help' => __d('cake_console', "Will bake a single action's file. core templates are (index, add, edit, view)") + ])->addArgument('alias', [ + 'help' => __d('cake_console', 'Will bake the template in but create the filename after .') + ])->addOption('plugin', [ + 'short' => 'p', + 'help' => __d('cake_console', 'Plugin to bake the view into.') + ])->addOption('admin', [ + 'help' => __d('cake_console', 'Set to only bake views for a prefix in Routing.prefixes'), + 'boolean' => true + ])->addOption('theme', [ + 'short' => 't', + 'help' => __d('cake_console', 'Theme to use when baking code.') + ])->addOption('connection', [ + 'short' => 'c', + 'help' => __d('cake_console', 'The connection the connected model is on.') + ])->addOption('force', [ + 'short' => 'f', + 'help' => __d('cake_console', 'Force overwriting existing files without prompting.') + ])->addSubcommand('all', [ + 'help' => __d('cake_console', 'Bake all CRUD action views for all controllers. Requires models and controllers to exist.') + ])->epilog( + __d('cake_console', 'Omitting all arguments and options will enter into an interactive mode.') + ); + + return $parser; + } } diff --git a/lib/Cake/Console/Command/TestShell.php b/lib/Cake/Console/Command/TestShell.php index 7d82c560..a571f1cd 100755 --- a/lib/Cake/Console/Command/TestShell.php +++ b/lib/Cake/Console/Command/TestShell.php @@ -28,7 +28,8 @@ * * @package Cake.Console.Command */ -class TestShell extends Shell { +class TestShell extends Shell +{ /** * Dispatcher object for the run. @@ -42,126 +43,127 @@ class TestShell extends Shell { * * @return ConsoleOptionParser */ - public function getOptionParser() { + public function getOptionParser() + { $parser = new ConsoleOptionParser($this->name); $parser->description( __d('cake_console', 'The CakePHP Testsuite allows you to run test cases from the command line') - )->addArgument('category', array( + )->addArgument('category', [ 'help' => __d('cake_console', 'The category for the test, or test file, to test.'), 'required' => false - ))->addArgument('file', array( + ])->addArgument('file', [ 'help' => __d('cake_console', 'The path to the file, or test file, to test.'), 'required' => false - ))->addOption('log-junit', array( + ])->addOption('log-junit', [ 'help' => __d('cake_console', ' Log test execution in JUnit XML format to file.'), 'default' => false - ))->addOption('log-json', array( + ])->addOption('log-json', [ 'help' => __d('cake_console', ' Log test execution in JSON format to file.'), 'default' => false - ))->addOption('log-tap', array( + ])->addOption('log-tap', [ 'help' => __d('cake_console', ' Log test execution in TAP format to file.'), 'default' => false - ))->addOption('log-dbus', array( + ])->addOption('log-dbus', [ 'help' => __d('cake_console', 'Log test execution to DBUS.'), 'default' => false - ))->addOption('coverage-html', array( + ])->addOption('coverage-html', [ 'help' => __d('cake_console', ' Generate code coverage report in HTML format.'), 'default' => false - ))->addOption('coverage-clover', array( + ])->addOption('coverage-clover', [ 'help' => __d('cake_console', ' Write code coverage data in Clover XML format.'), 'default' => false - ))->addOption('coverage-text', array( + ])->addOption('coverage-text', [ 'help' => __d('cake_console', 'Output code coverage report in Text format.'), 'boolean' => true - ))->addOption('testdox-html', array( + ])->addOption('testdox-html', [ 'help' => __d('cake_console', ' Write agile documentation in HTML format to file.'), 'default' => false - ))->addOption('testdox-text', array( + ])->addOption('testdox-text', [ 'help' => __d('cake_console', ' Write agile documentation in Text format to file.'), 'default' => false - ))->addOption('filter', array( + ])->addOption('filter', [ 'help' => __d('cake_console', ' Filter which tests to run.'), 'default' => false - ))->addOption('group', array( + ])->addOption('group', [ 'help' => __d('cake_console', ' Only runs tests from the specified group(s).'), 'default' => false - ))->addOption('exclude-group', array( + ])->addOption('exclude-group', [ 'help' => __d('cake_console', ' Exclude tests from the specified group(s).'), 'default' => false - ))->addOption('list-groups', array( + ])->addOption('list-groups', [ 'help' => __d('cake_console', 'List available test groups.'), 'boolean' => true - ))->addOption('loader', array( + ])->addOption('loader', [ 'help' => __d('cake_console', 'TestSuiteLoader implementation to use.'), 'default' => false - ))->addOption('repeat', array( + ])->addOption('repeat', [ 'help' => __d('cake_console', ' Runs the test(s) repeatedly.'), 'default' => false - ))->addOption('tap', array( + ])->addOption('tap', [ 'help' => __d('cake_console', 'Report test execution progress in TAP format.'), 'boolean' => true - ))->addOption('testdox', array( + ])->addOption('testdox', [ 'help' => __d('cake_console', 'Report test execution progress in TestDox format.'), 'default' => false, 'boolean' => true - ))->addOption('no-colors', array( + ])->addOption('no-colors', [ 'help' => __d('cake_console', 'Do not use colors in output.'), 'boolean' => true - ))->addOption('stderr', array( + ])->addOption('stderr', [ 'help' => __d('cake_console', 'Write to STDERR instead of STDOUT.'), 'boolean' => true - ))->addOption('stop-on-error', array( + ])->addOption('stop-on-error', [ 'help' => __d('cake_console', 'Stop execution upon first error or failure.'), 'boolean' => true - ))->addOption('stop-on-failure', array( + ])->addOption('stop-on-failure', [ 'help' => __d('cake_console', 'Stop execution upon first failure.'), 'boolean' => true - ))->addOption('stop-on-skipped', array( + ])->addOption('stop-on-skipped', [ 'help' => __d('cake_console', 'Stop execution upon first skipped test.'), 'boolean' => true - ))->addOption('stop-on-incomplete', array( + ])->addOption('stop-on-incomplete', [ 'help' => __d('cake_console', 'Stop execution upon first incomplete test.'), 'boolean' => true - ))->addOption('strict', array( + ])->addOption('strict', [ 'help' => __d('cake_console', 'Mark a test as incomplete if no assertions are made.'), 'boolean' => true - ))->addOption('wait', array( + ])->addOption('wait', [ 'help' => __d('cake_console', 'Waits for a keystroke after each test.'), 'boolean' => true - ))->addOption('process-isolation', array( + ])->addOption('process-isolation', [ 'help' => __d('cake_console', 'Run each test in a separate PHP process.'), 'boolean' => true - ))->addOption('no-globals-backup', array( + ])->addOption('no-globals-backup', [ 'help' => __d('cake_console', 'Do not backup and restore $GLOBALS for each test.'), 'boolean' => true - ))->addOption('static-backup', array( + ])->addOption('static-backup', [ 'help' => __d('cake_console', 'Backup and restore static attributes for each test.'), 'boolean' => true - ))->addOption('syntax-check', array( + ])->addOption('syntax-check', [ 'help' => __d('cake_console', 'Try to check source files for syntax errors.'), 'boolean' => true - ))->addOption('bootstrap', array( + ])->addOption('bootstrap', [ 'help' => __d('cake_console', ' A "bootstrap" PHP file that is run before the tests.'), 'default' => false - ))->addOption('configuration', array( + ])->addOption('configuration', [ 'help' => __d('cake_console', ' Read configuration from XML file.'), 'default' => false - ))->addOption('no-configuration', array( + ])->addOption('no-configuration', [ 'help' => __d('cake_console', 'Ignore default configuration file (phpunit.xml).'), 'boolean' => true - ))->addOption('include-path', array( + ])->addOption('include-path', [ 'help' => __d('cake_console', ' Prepend PHP include_path with given path(s).'), 'default' => false - ))->addOption('directive', array( + ])->addOption('directive', [ 'help' => __d('cake_console', 'key[=value] Sets a php.ini value.'), 'short' => 'd', 'default' => false - ))->addOption('fixture', array( + ])->addOption('fixture', [ 'help' => __d('cake_console', 'Choose a custom fixture manager.') - ))->addOption('debug', array( + ])->addOption('debug', [ 'help' => __d('cake_console', 'More verbose output.') - )); + ]); return $parser; } @@ -172,7 +174,8 @@ public function getOptionParser() { * @return void * @throws Exception */ - public function initialize() { + public function initialize() + { $this->_dispatcher = new CakeTestSuiteDispatcher(); $success = $this->_dispatcher->loadTestFramework(); if (!$success) { @@ -180,21 +183,41 @@ public function initialize() { } } + /** + * Main entry point to this shell + * + * @return void + */ + public function main() + { + $this->out(__d('cake_console', 'CakePHP Test Shell')); + $this->hr(); + + $args = $this->_parseArgs(); + + if (empty($args['case'])) { + return $this->available(); + } + + $this->_run($args, $this->_runnerOptions()); + } + /** * Parse the CLI options into an array CakeTestDispatcher can use. * * @return array|null Array of params for CakeTestDispatcher or null. */ - protected function _parseArgs() { + protected function _parseArgs() + { if (empty($this->args)) { return null; } - $params = array( + $params = [ 'core' => false, 'app' => false, 'plugin' => null, 'output' => 'text', - ); + ]; if (strpos($this->args[0], '.php')) { $category = $this->_mapFileToCategory($this->args[0]); @@ -208,7 +231,7 @@ protected function _parseArgs() { if ($category === 'core') { $params['core'] = true; - } elseif ($category === 'app') { + } else if ($category === 'app') { $params['app'] = true; } else { $params['plugin'] = $category; @@ -218,126 +241,25 @@ protected function _parseArgs() { } /** - * Converts the options passed to the shell as options for the PHPUnit cli runner - * - * @return array Array of params for CakeTestDispatcher - */ - protected function _runnerOptions() { - $options = array(); - $params = $this->params; - unset($params['help']); - unset($params['quiet']); - - if (!empty($params['no-colors'])) { - unset($params['no-colors'], $params['colors']); - } else { - $params['colors'] = true; - } - - foreach ($params as $param => $value) { - if ($value === false) { - continue; - } - if ($param === 'directive') { - $options[] = '-d'; - } else { - $options[] = '--' . $param; - } - if (is_string($value)) { - $options[] = $value; - } - } - return $options; - } - - /** - * Main entry point to this shell - * - * @return void - */ - public function main() { - $this->out(__d('cake_console', 'CakePHP Test Shell')); - $this->hr(); - - $args = $this->_parseArgs(); - - if (empty($args['case'])) { - return $this->available(); - } - - $this->_run($args, $this->_runnerOptions()); - } - - /** - * Runs the test case from $runnerArgs - * - * @param array $runnerArgs list of arguments as obtained from _parseArgs() - * @param array $options list of options as constructed by _runnerOptions() - * @return void - */ - protected function _run($runnerArgs, $options = array()) { - restore_error_handler(); - restore_error_handler(); - - $testCli = new CakeTestSuiteCommand('CakeTestLoader', $runnerArgs); - $testCli->run($options); - } - - /** - * Shows a list of available test cases and gives the option to run one of them + * For the given file, what category of test is it? returns app, core or the name of the plugin * - * @return void + * @param string $file The file to map. + * @return string */ - public function available() { - $params = $this->_parseArgs(); - $testCases = CakeTestLoader::generateTestList($params); - $app = isset($params['app']) ? $params['app'] : null; - $plugin = isset($params['plugin']) ? $params['plugin'] : null; - - $title = "Core Test Cases:"; - $category = 'core'; - if ($app) { - $title = "App Test Cases:"; - $category = 'app'; - } elseif ($plugin) { - $title = Inflector::humanize($plugin) . " Test Cases:"; - $category = $plugin; - } - - if (empty($testCases)) { - $this->out(__d('cake_console', "No test cases available \n\n")); - return $this->out($this->OptionParser->help()); - } - - $this->out($title); - $i = 1; - $cases = array(); - foreach ($testCases as $testCase) { - $case = str_replace('Test.php', '', $testCase); - $this->out("[$i] $case"); - $cases[$i] = $case; - $i++; + protected function _mapFileToCategory($file) + { + $_file = realpath($file); + if ($_file) { + $file = $_file; } - while ($choice = $this->in(__d('cake_console', 'What test case would you like to run?'), null, 'q')) { - if (is_numeric($choice) && isset($cases[$choice])) { - $this->args[0] = $category; - $this->args[1] = $cases[$choice]; - $this->_run($this->_parseArgs(), $this->_runnerOptions()); - break; - } - - if (is_string($choice) && in_array($choice, $cases)) { - $this->args[0] = $category; - $this->args[1] = $choice; - $this->_run($this->_parseArgs(), $this->_runnerOptions()); - break; - } - - if ($choice === 'q') { - break; - } + $file = str_replace(DS, '/', $file); + if (strpos($file, 'lib/Cake/') !== false) { + return 'core'; + } else if (preg_match('@(?:plugins|Plugin)/([^/]*)@', $file, $match)) { + return $match[1]; } + return 'app'; } /** @@ -349,7 +271,8 @@ public function available() { * @return array array(type, case) * @throws Exception */ - protected function _mapFileToCase($file, $category, $throwOnMissingFile = true) { + protected function _mapFileToCase($file, $category, $throwOnMissingFile = true) + { if (!$category || (substr($file, -4) !== '.php')) { return false; } @@ -413,24 +336,111 @@ protected function _mapFileToCase($file, $category, $throwOnMissingFile = true) } /** - * For the given file, what category of test is it? returns app, core or the name of the plugin + * Shows a list of available test cases and gives the option to run one of them * - * @param string $file The file to map. - * @return string + * @return void */ - protected function _mapFileToCategory($file) { - $_file = realpath($file); - if ($_file) { - $file = $_file; + public function available() + { + $params = $this->_parseArgs(); + $testCases = CakeTestLoader::generateTestList($params); + $app = isset($params['app']) ? $params['app'] : null; + $plugin = isset($params['plugin']) ? $params['plugin'] : null; + + $title = "Core Test Cases:"; + $category = 'core'; + if ($app) { + $title = "App Test Cases:"; + $category = 'app'; + } else if ($plugin) { + $title = Inflector::humanize($plugin) . " Test Cases:"; + $category = $plugin; } - $file = str_replace(DS, '/', $file); - if (strpos($file, 'lib/Cake/') !== false) { - return 'core'; - } elseif (preg_match('@(?:plugins|Plugin)/([^/]*)@', $file, $match)) { - return $match[1]; + if (empty($testCases)) { + $this->out(__d('cake_console', "No test cases available \n\n")); + return $this->out($this->OptionParser->help()); } - return 'app'; + + $this->out($title); + $i = 1; + $cases = []; + foreach ($testCases as $testCase) { + $case = str_replace('Test.php', '', $testCase); + $this->out("[$i] $case"); + $cases[$i] = $case; + $i++; + } + + while ($choice = $this->in(__d('cake_console', 'What test case would you like to run?'), null, 'q')) { + if (is_numeric($choice) && isset($cases[$choice])) { + $this->args[0] = $category; + $this->args[1] = $cases[$choice]; + $this->_run($this->_parseArgs(), $this->_runnerOptions()); + break; + } + + if (is_string($choice) && in_array($choice, $cases)) { + $this->args[0] = $category; + $this->args[1] = $choice; + $this->_run($this->_parseArgs(), $this->_runnerOptions()); + break; + } + + if ($choice === 'q') { + break; + } + } + } + + /** + * Runs the test case from $runnerArgs + * + * @param array $runnerArgs list of arguments as obtained from _parseArgs() + * @param array $options list of options as constructed by _runnerOptions() + * @return void + */ + protected function _run($runnerArgs, $options = []) + { + restore_error_handler(); + restore_error_handler(); + + $testCli = new CakeTestSuiteCommand('CakeTestLoader', $runnerArgs); + $testCli->run($options); + } + + /** + * Converts the options passed to the shell as options for the PHPUnit cli runner + * + * @return array Array of params for CakeTestDispatcher + */ + protected function _runnerOptions() + { + $options = []; + $params = $this->params; + unset($params['help']); + unset($params['quiet']); + + if (!empty($params['no-colors'])) { + unset($params['no-colors'], $params['colors']); + } else { + $params['colors'] = true; + } + + foreach ($params as $param => $value) { + if ($value === false) { + continue; + } + if ($param === 'directive') { + $options[] = '-d'; + } else { + $options[] = '--' . $param; + } + if (is_string($value)) { + $options[] = $value; + } + } + return $options; } } \ No newline at end of file diff --git a/lib/Cake/Console/Command/TestsuiteShell.php b/lib/Cake/Console/Command/TestsuiteShell.php index 911f787c..c0a5d6e8 100755 --- a/lib/Cake/Console/Command/TestsuiteShell.php +++ b/lib/Cake/Console/Command/TestsuiteShell.php @@ -29,72 +29,76 @@ * * @package Cake.Console.Command */ -class TestsuiteShell extends TestShell { +class TestsuiteShell extends TestShell +{ -/** - * Gets the option parser instance and configures it. - * - * @return ConsoleOptionParser - */ - public function getOptionParser() { - $parser = parent::getOptionParser(); + /** + * Gets the option parser instance and configures it. + * + * @return ConsoleOptionParser + */ + public function getOptionParser() + { + $parser = parent::getOptionParser(); - $parser->description(array( - __d('cake_console', 'The CakePHP Testsuite allows you to run test cases from the command line'), - __d('cake_console', "This shell is for backwards-compatibility only\nuse the test shell instead") - )); + $parser->description([ + __d('cake_console', 'The CakePHP Testsuite allows you to run test cases from the command line'), + __d('cake_console', "This shell is for backwards-compatibility only\nuse the test shell instead") + ]); - return $parser; - } + return $parser; + } -/** - * Parse the CLI options into an array CakeTestDispatcher can use. - * - * @return array Array of params for CakeTestDispatcher - */ - protected function _parseArgs() { - if (empty($this->args)) { - return; - } - $params = array( - 'core' => false, - 'app' => false, - 'plugin' => null, - 'output' => 'text', - ); + /** + * Main entry point to this shell + * + * @return void + */ + public function main() + { + $this->out(__d('cake_console', 'CakePHP Test Shell')); + $this->hr(); - $category = $this->args[0]; + $args = $this->_parseArgs(); - if ($category === 'core') { - $params['core'] = true; - } elseif ($category === 'app') { - $params['app'] = true; - } elseif ($category !== 'core') { - $params['plugin'] = $category; - } + if (empty($args['case'])) { + return $this->available(); + } - if (isset($this->args[1])) { - $params['case'] = $this->args[1]; - } - return $params; - } + $this->_run($args, $this->_runnerOptions()); + } -/** - * Main entry point to this shell - * - * @return void - */ - public function main() { - $this->out(__d('cake_console', 'CakePHP Test Shell')); - $this->hr(); + /** + * Parse the CLI options into an array CakeTestDispatcher can use. + * + * @return array Array of params for CakeTestDispatcher + */ + protected function _parseArgs() + { + if (empty($this->args)) { + return; + } + $params = [ + 'core' => false, + 'app' => false, + 'plugin' => null, + 'output' => 'text', + ]; - $args = $this->_parseArgs(); + $category = $this->args[0]; - if (empty($args['case'])) { - return $this->available(); - } + if ($category === 'core') { + $params['core'] = true; + } else if ($category === 'app') { + $params['app'] = true; + } else if ($category !== 'core') { + $params['plugin'] = $category; + } - $this->_run($args, $this->_runnerOptions()); - } + if (isset($this->args[1])) { + $params['case'] = $this->args[1]; + } + return $params; + } } diff --git a/lib/Cake/Console/Command/UpgradeShell.php b/lib/Cake/Console/Command/UpgradeShell.php index f9d1223a..9fe36760 100755 --- a/lib/Cake/Console/Command/UpgradeShell.php +++ b/lib/Cake/Console/Command/UpgradeShell.php @@ -25,869 +25,890 @@ * * @package Cake.Console.Command */ -class UpgradeShell extends AppShell { - -/** - * Files - * - * @var array - */ - protected $_files = array(); - -/** - * Paths - * - * @var array - */ - protected $_paths = array(); - -/** - * Map - * - * @var array - */ - protected $_map = array( - 'Controller' => 'Controller', - 'Component' => 'Controller/Component', - 'Model' => 'Model', - 'Behavior' => 'Model/Behavior', - 'Datasource' => 'Model/Datasource', - 'Dbo' => 'Model/Datasource/Database', - 'View' => 'View', - 'Helper' => 'View/Helper', - 'Shell' => 'Console/Command', - 'Task' => 'Console/Command/Task', - 'Case' => 'Test/Case', - 'Fixture' => 'Test/Fixture', - 'Error' => 'Lib/Error', - ); - -/** - * Shell startup, prints info message about dry run. - * - * @return void - */ - public function startup() { - parent::startup(); - if ($this->params['dry-run']) { - $this->out(__d('cake_console', 'Dry-run mode enabled!'), 1, Shell::QUIET); - } - if ($this->params['git'] && !is_dir('.git')) { - $this->out(__d('cake_console', 'No git repository detected!'), 1, Shell::QUIET); - } - } - -/** - * Run all upgrade steps one at a time - * - * @return void - */ - public function all() { - foreach ($this->OptionParser->subcommands() as $command) { - $name = $command->name(); - if ($name === 'all') { - continue; - } - $this->out(__d('cake_console', 'Running %s', $name)); - $this->$name(); - } - } - -/** - * Update tests. - * - * - Update tests class names to FooTest rather than FooTestCase. - * - * @return void - */ - public function tests() { - $this->_paths = array(APP . 'tests' . DS); - if (!empty($this->params['plugin'])) { - $this->_paths = array(CakePlugin::path($this->params['plugin']) . 'tests' . DS); - } - $patterns = array( - array( - '*TestCase extends CakeTestCase to *Test extends CakeTestCase', - '/([a-zA-Z]*Test)Case extends CakeTestCase/', - '\1 extends CakeTestCase' - ), - ); - - $this->_filesRegexpUpdate($patterns); - } - -/** - * Move files and folders to their new homes - * - * Moves folders containing files which cannot necessarily be auto-detected (libs and templates) - * and then looks for all php files except vendors, and moves them to where Cake 2.0 expects - * to find them. - * - * @return void - */ - public function locations() { - $cwd = getcwd(); - - if (!empty($this->params['plugin'])) { - chdir(CakePlugin::path($this->params['plugin'])); - } - - if (is_dir('plugins')) { - $Folder = new Folder('plugins'); - list($plugins) = $Folder->read(); - foreach ($plugins as $plugin) { - chdir($cwd . DS . 'plugins' . DS . $plugin); - $this->out(__d('cake_console', 'Upgrading locations for plugin %s', $plugin)); - $this->locations(); - } - $this->_files = array(); - chdir($cwd); - $this->out(__d('cake_console', 'Upgrading locations for app directory')); - } - $moves = array( - 'config' => 'Config', - 'Config' . DS . 'schema' => 'Config' . DS . 'Schema', - 'libs' => 'Lib', - 'tests' => 'Test', - 'views' => 'View', - 'models' => 'Model', - 'Model' . DS . 'behaviors' => 'Model' . DS . 'Behavior', - 'Model' . DS . 'datasources' => 'Model' . DS . 'Datasource', - 'Test' . DS . 'cases' => 'Test' . DS . 'Case', - 'Test' . DS . 'fixtures' => 'Test' . DS . 'Fixture', - 'vendors' . DS . 'shells' . DS . 'templates' => 'Console' . DS . 'Templates', - ); - foreach ($moves as $old => $new) { - if (is_dir($old)) { - $this->out(__d('cake_console', 'Moving %s to %s', $old, $new)); - if (!$this->params['dry-run']) { - if ($this->params['git']) { - exec('git mv -f ' . escapeshellarg($old) . ' ' . escapeshellarg($old . '__')); - exec('git mv -f ' . escapeshellarg($old . '__') . ' ' . escapeshellarg($new)); - } else { - $Folder = new Folder($old); - $Folder->move($new); - } - } - } - } - - $this->_moveViewFiles(); - $this->_moveAppClasses(); - - $sourceDirs = array( - '.' => array('recursive' => false), - 'Console', - 'controllers', - 'Controller', - 'Lib' => array('checkFolder' => false), - 'models', - 'Model', - 'tests', - 'Test' => array('regex' => '@class (\S*Test) extends CakeTestCase@'), - 'views', - 'View', - 'vendors/shells', - ); - - $defaultOptions = array( - 'recursive' => true, - 'checkFolder' => true, - 'regex' => '@class (\S*) .*(\s|\v)*{@i' - ); - foreach ($sourceDirs as $dir => $options) { - if (is_numeric($dir)) { - $dir = $options; - $options = array(); - } - $options += $defaultOptions; - $this->_movePhpFiles($dir, $options); - } - } - -/** - * Update helpers. - * - * - Converts helpers usage to new format. - * - * @return void - */ - public function helpers() { - $this->_paths = array_diff(App::path('views'), App::core('views')); - - if (!empty($this->params['plugin'])) { - $this->_paths = array(CakePlugin::path($this->params['plugin']) . 'views' . DS); - } - - $patterns = array(); - App::build(array( - 'View/Helper' => App::core('View/Helper'), - ), App::APPEND); - $helpers = App::objects('helper'); - $plugins = App::objects('plugin'); - $pluginHelpers = array(); - foreach ($plugins as $plugin) { - CakePlugin::load($plugin); - $pluginHelpers = array_merge( - $pluginHelpers, - App::objects('helper', CakePlugin::path($plugin) . DS . 'views' . DS . 'helpers' . DS, false) - ); - } - $helpers = array_merge($pluginHelpers, $helpers); - foreach ($helpers as $helper) { - $helper = preg_replace('/Helper$/', '', $helper); - $oldHelper = $helper; - $oldHelper{0} = strtolower($oldHelper{0}); - $patterns[] = array( - "\${$oldHelper} to \$this->{$helper}", - "/\\\${$oldHelper}->/", - "\\\$this->{$helper}->" - ); - } - - $this->_filesRegexpUpdate($patterns); - } - -/** - * Update i18n. - * - * - Removes extra true param. - * - Add the echo to __*() calls that didn't need them before. - * - * @return void - */ - public function i18n() { - $this->_paths = array( - APP - ); - if (!empty($this->params['plugin'])) { - $this->_paths = array(CakePlugin::path($this->params['plugin'])); - } - - $patterns = array( - array( - '_filesRegexpUpdate($patterns); - } - -/** - * Upgrade the removed basics functions. - * - * - a(*) -> array(*) - * - e(*) -> echo * - * - ife(*, *, *) -> !empty(*) ? * : * - * - a(*) -> array(*) - * - r(*, *, *) -> str_replace(*, *, *) - * - up(*) -> strtoupper(*) - * - low(*, *, *) -> strtolower(*) - * - getMicrotime() -> microtime(true) - * - * @return void - */ - public function basics() { - $this->_paths = array( - APP - ); - if (!empty($this->params['plugin'])) { - $this->_paths = array(CakePlugin::path($this->params['plugin'])); - } - $patterns = array( - array( - 'a(*) -> array(*)', - '/\ba\((.*)\)/', - 'array(\1)' - ), - array( - 'e(*) -> echo *', - '/\be\((.*)\)/', - 'echo \1' - ), - array( - 'ife(*, *, *) -> !empty(*) ? * : *', - '/ife\((.*), (.*), (.*)\)/', - '!empty(\1) ? \2 : \3' - ), - array( - 'r(*, *, *) -> str_replace(*, *, *)', - '/\br\(/', - 'str_replace(' - ), - array( - 'up(*) -> strtoupper(*)', - '/\bup\(/', - 'strtoupper(' - ), - array( - 'low(*) -> strtolower(*)', - '/\blow\(/', - 'strtolower(' - ), - array( - 'getMicrotime() -> microtime(true)', - '/getMicrotime\(\)/', - 'microtime(true)' - ), - ); - $this->_filesRegexpUpdate($patterns); - } - -/** - * Update the properties moved to CakeRequest. - * - * @return void - */ - public function request() { - $views = array_diff(App::path('views'), App::core('views')); - $controllers = array_diff(App::path('controllers'), App::core('controllers'), array(APP)); - $components = array_diff(App::path('components'), App::core('components')); - - $this->_paths = array_merge($views, $controllers, $components); - - if (!empty($this->params['plugin'])) { - $pluginPath = CakePlugin::path($this->params['plugin']); - $this->_paths = array( - $pluginPath . 'controllers' . DS, - $pluginPath . 'controllers' . DS . 'components' . DS, - $pluginPath . 'views' . DS, - ); - } - $patterns = array( - array( - '$this->data -> $this->request->data', - '/(\$this->data\b(?!\())/', - '$this->request->data' - ), - array( - '$this->params -> $this->request->params', - '/(\$this->params\b(?!\())/', - '$this->request->params' - ), - array( - '$this->webroot -> $this->request->webroot', - '/(\$this->webroot\b(?!\())/', - '$this->request->webroot' - ), - array( - '$this->base -> $this->request->base', - '/(\$this->base\b(?!\())/', - '$this->request->base' - ), - array( - '$this->here -> $this->request->here', - '/(\$this->here\b(?!\())/', - '$this->request->here' - ), - array( - '$this->action -> $this->request->action', - '/(\$this->action\b(?!\())/', - '$this->request->action' - ), - array( - '$this->request->onlyAllow() -> $this->request->allowMethod()', - '/\$this->request->onlyAllow\(/', - '$this->request->allowMethod(' - ) - ); - $this->_filesRegexpUpdate($patterns); - } - -/** - * Update Configure::read() calls with no params. - * - * @return void - */ - public function configure() { - $this->_paths = array( - APP - ); - if (!empty($this->params['plugin'])) { - $this->_paths = array(CakePlugin::path($this->params['plugin'])); - } - $patterns = array( - array( - "Configure::read() -> Configure::read('debug')", - '/Configure::read\(\)/', - 'Configure::read(\'debug\')' - ), - ); - $this->_filesRegexpUpdate($patterns); - } - -/** - * constants - * - * @return void - */ - public function constants() { - $this->_paths = array( - APP - ); - if (!empty($this->params['plugin'])) { - $this->_paths = array(CakePlugin::path($this->params['plugin'])); - } - $patterns = array( - array( - "LIBS -> CAKE", - '/\bLIBS\b/', - 'CAKE' - ), - array( - "CONFIGS -> APP . 'Config' . DS", - '/\bCONFIGS\b/', - 'APP . \'Config\' . DS' - ), - array( - "CONTROLLERS -> APP . 'Controller' . DS", - '/\bCONTROLLERS\b/', - 'APP . \'Controller\' . DS' - ), - array( - "COMPONENTS -> APP . 'Controller' . DS . 'Component' . DS", - '/\bCOMPONENTS\b/', - 'APP . \'Controller\' . DS . \'Component\'' - ), - array( - "MODELS -> APP . 'Model' . DS", - '/\bMODELS\b/', - 'APP . \'Model\' . DS' - ), - array( - "BEHAVIORS -> APP . 'Model' . DS . 'Behavior' . DS", - '/\bBEHAVIORS\b/', - 'APP . \'Model\' . DS . \'Behavior\' . DS' - ), - array( - "VIEWS -> APP . 'View' . DS", - '/\bVIEWS\b/', - 'APP . \'View\' . DS' - ), - array( - "HELPERS -> APP . 'View' . DS . 'Helper' . DS", - '/\bHELPERS\b/', - 'APP . \'View\' . DS . \'Helper\' . DS' - ), - array( - "LAYOUTS -> APP . 'View' . DS . 'Layouts' . DS", - '/\bLAYOUTS\b/', - 'APP . \'View\' . DS . \'Layouts\' . DS' - ), - array( - "ELEMENTS -> APP . 'View' . DS . 'Elements' . DS", - '/\bELEMENTS\b/', - 'APP . \'View\' . DS . \'Elements\' . DS' - ), - array( - "CONSOLE_LIBS -> CAKE . 'Console' . DS", - '/\bCONSOLE_LIBS\b/', - 'CAKE . \'Console\' . DS' - ), - array( - "CAKE_TESTS_LIB -> CAKE . 'TestSuite' . DS", - '/\bCAKE_TESTS_LIB\b/', - 'CAKE . \'TestSuite\' . DS' - ), - array( - "CAKE_TESTS -> CAKE . 'Test' . DS", - '/\bCAKE_TESTS\b/', - 'CAKE . \'Test\' . DS' - ) - ); - $this->_filesRegexpUpdate($patterns); - } - -/** - * Update controller redirects. - * - * - Make redirect statements return early. - * - * @return void - */ - public function controller_redirects() { - $this->_paths = App::Path('Controller'); - if (!empty($this->params['plugin'])) { - $this->_paths = App::Path('Controller', $this->params['plugin']); - } - $patterns = array( - array( - '$this->redirect() to return $this->redirect()', - '/\t\$this-\>redirect\(/', - "\t" . 'return $this->redirect(' - ), - ); - - $this->_filesRegexpUpdate($patterns); - } - -/** - * Update components. - * - * - Make components that extend CakeObject to extend Component. - * - * @return void - */ - public function components() { - $this->_paths = App::Path('Controller/Component'); - if (!empty($this->params['plugin'])) { - $this->_paths = App::Path('Controller/Component', $this->params['plugin']); - } - $patterns = array( - array( - '*Component extends Object to *Component extends Component', - '/([a-zA-Z]*Component extends) Object/', - '\1 Component' - ), - array( - '*Component extends CakeObject to *Component extends Component', - '/([a-zA-Z]*Component extends) CakeObject/', - '\1 Component' - ), - ); - - $this->_filesRegexpUpdate($patterns); - } - -/** - * Replace cakeError with built-in exceptions. - * NOTE: this ignores calls where you've passed your own secondary parameters to cakeError(). - * - * @return void - */ - public function exceptions() { - $controllers = array_diff(App::path('controllers'), App::core('controllers'), array(APP)); - $components = array_diff(App::path('components'), App::core('components')); - - $this->_paths = array_merge($controllers, $components); - - if (!empty($this->params['plugin'])) { - $pluginPath = CakePlugin::path($this->params['plugin']); - $this->_paths = array( - $pluginPath . 'controllers' . DS, - $pluginPath . 'controllers' . DS . 'components' . DS, - ); - } - $patterns = array( - array( - '$this->cakeError("error400") -> throw new BadRequestException()', - '/(\$this->cakeError\(["\']error400["\']\));/', - 'throw new BadRequestException();' - ), - array( - '$this->cakeError("error404") -> throw new NotFoundException()', - '/(\$this->cakeError\(["\']error404["\']\));/', - 'throw new NotFoundException();' - ), - array( - '$this->cakeError("error500") -> throw new InternalErrorException()', - '/(\$this->cakeError\(["\']error500["\']\));/', - 'throw new InternalErrorException();' - ), - ); - $this->_filesRegexpUpdate($patterns); - } - -/** - * Move application views files to where they now should be - * - * Find all view files in the folder and determine where cake expects the file to be - * - * @return void - */ - protected function _moveViewFiles() { - if (!is_dir('View')) { - return; - } - - $dirs = scandir('View'); - foreach ($dirs as $old) { - if (!is_dir('View' . DS . $old) || $old === '.' || $old === '..') { - continue; - } - - $new = 'View' . DS . Inflector::camelize($old); - $old = 'View' . DS . $old; - if ($new === $old) { - continue; - } - - $this->out(__d('cake_console', 'Moving %s to %s', $old, $new)); - if (!$this->params['dry-run']) { - if ($this->params['git']) { - exec('git mv -f ' . escapeshellarg($old) . ' ' . escapeshellarg($old . '__')); - exec('git mv -f ' . escapeshellarg($old . '__') . ' ' . escapeshellarg($new)); - } else { - $Folder = new Folder($old); - $Folder->move($new); - } - } - } - } - -/** - * Move the AppController, and AppModel classes. - * - * @return void - */ - protected function _moveAppClasses() { - $files = array( - APP . 'app_controller.php' => APP . 'Controller' . DS . 'AppController.php', - APP . 'controllers' . DS . 'app_controller.php' => APP . 'Controller' . DS . 'AppController.php', - APP . 'app_model.php' => APP . 'Model' . DS . 'AppModel.php', - APP . 'models' . DS . 'app_model.php' => APP . 'Model' . DS . 'AppModel.php', - ); - foreach ($files as $old => $new) { - if (file_exists($old)) { - $this->out(__d('cake_console', 'Moving %s to %s', $old, $new)); - - if ($this->params['dry-run']) { - continue; - } - if ($this->params['git']) { - exec('git mv -f ' . escapeshellarg($old) . ' ' . escapeshellarg($old . '__')); - exec('git mv -f ' . escapeshellarg($old . '__') . ' ' . escapeshellarg($new)); - } else { - rename($old, $new); - } - } - } - } - -/** - * Move application php files to where they now should be - * - * Find all php files in the folder (honoring recursive) and determine where CakePHP expects the file to be - * If the file is not exactly where CakePHP expects it - move it. - * - * @param string $path The path to move files in. - * @param array $options array(recursive, checkFolder) - * @return void - */ - protected function _movePhpFiles($path, $options) { - if (!is_dir($path)) { - return; - } - - $paths = $this->_paths; - - $this->_paths = array($path); - $this->_files = array(); - if ($options['recursive']) { - $this->_findFiles('php'); - } else { - $this->_files = scandir($path); - foreach ($this->_files as $i => $file) { - if (strlen($file) < 5 || substr($file, -4) !== '.php') { - unset($this->_files[$i]); - } - } - } - - $cwd = getcwd(); - foreach ($this->_files as &$file) { - $file = $cwd . DS . $file; - - $contents = file_get_contents($file); - preg_match($options['regex'], $contents, $match); - if (!$match) { - continue; - } - - $class = $match[1]; - - if (substr($class, 0, 3) === 'Dbo') { - $type = 'Dbo'; - } else { - preg_match('@([A-Z][^A-Z]*)$@', $class, $match); - if ($match) { - $type = $match[1]; - } else { - $type = 'unknown'; - } - } - - preg_match('@^.*[\\\/]plugins[\\\/](.*?)[\\\/]@', $file, $match); - $base = $cwd . DS; - $plugin = false; - if ($match) { - $base = $match[0]; - $plugin = $match[1]; - } - - if ($options['checkFolder'] && !empty($this->_map[$type])) { - $folder = str_replace('/', DS, $this->_map[$type]); - $new = $base . $folder . DS . $class . '.php'; - } else { - $new = dirname($file) . DS . $class . '.php'; - } - - if ($file === $new) { - continue; - } - - $dir = dirname($new); - if (!is_dir($dir)) { - new Folder($dir, true); - } - - $this->out(__d('cake_console', 'Moving %s to %s', $file, $new), 1, Shell::VERBOSE); - if (!$this->params['dry-run']) { - if ($this->params['git']) { - exec('git mv -f ' . escapeshellarg($file) . ' ' . escapeshellarg($file . '__')); - exec('git mv -f ' . escapeshellarg($file . '__') . ' ' . escapeshellarg($new)); - } else { - rename($file, $new); - } - } - } - - $this->_paths = $paths; - } - -/** - * Updates files based on regular expressions. - * - * @param array $patterns Array of search and replacement patterns. - * @return void - */ - protected function _filesRegexpUpdate($patterns) { - $this->_findFiles($this->params['ext']); - foreach ($this->_files as $file) { - $this->out(__d('cake_console', 'Updating %s...', $file), 1, Shell::VERBOSE); - $this->_updateFile($file, $patterns); - } - } - -/** - * Searches the paths and finds files based on extension. - * - * @param string $extensions The extensions to include. Defaults to none. - * @return void - */ - protected function _findFiles($extensions = '') { - $this->_files = array(); - foreach ($this->_paths as $path) { - if (!is_dir($path)) { - continue; - } - $Iterator = new RegexIterator( - new RecursiveIteratorIterator(new RecursiveDirectoryIterator($path)), - '/^.+\.(' . $extensions . ')$/i', - RegexIterator::MATCH - ); - foreach ($Iterator as $file) { - if ($file->isFile()) { - $this->_files[] = $file->getPathname(); - } - } - } - } - -/** - * Update a single file. - * - * @param string $file The file to update - * @param array $patterns The replacement patterns to run. - * @return void - */ - protected function _updateFile($file, $patterns) { - $contents = file_get_contents($file); - - foreach ($patterns as $pattern) { - $this->out(__d('cake_console', ' * Updating %s', $pattern[0]), 1, Shell::VERBOSE); - $contents = preg_replace($pattern[1], $pattern[2], $contents); - } - - $this->out(__d('cake_console', 'Done updating %s', $file), 1); - if (!$this->params['dry-run']) { - file_put_contents($file, $contents); - } - } - -/** - * Gets the option parser instance and configures it. - * - * @return ConsoleOptionParser - */ - public function getOptionParser() { - $parser = parent::getOptionParser(); - - $subcommandParser = array( - 'options' => array( - 'plugin' => array( - 'short' => 'p', - 'help' => __d('cake_console', 'The plugin to update. Only the specified plugin will be updated.') - ), - 'ext' => array( - 'short' => 'e', - 'help' => __d('cake_console', 'The extension(s) to search. A pipe delimited list, or a preg_match compatible subpattern'), - 'default' => 'php|ctp|thtml|inc|tpl' - ), - 'git' => array( - 'short' => 'g', - 'help' => __d('cake_console', 'Use git command for moving files around.'), - 'boolean' => true - ), - 'dry-run' => array( - 'short' => 'd', - 'help' => __d('cake_console', 'Dry run the update, no files will actually be modified.'), - 'boolean' => true - ) - ) - ); - - $parser->description( - __d('cake_console', "A tool to help automate upgrading an application or plugin " . - "from CakePHP 1.3 to 2.0. Be sure to have a backup of your application before " . - "running these commands." - ))->addSubcommand('all', array( - 'help' => __d('cake_console', 'Run all upgrade commands.'), - 'parser' => $subcommandParser - ))->addSubcommand('tests', array( - 'help' => __d('cake_console', 'Update tests class names to FooTest rather than FooTestCase.'), - 'parser' => $subcommandParser - ))->addSubcommand('locations', array( - 'help' => __d('cake_console', 'Move files and folders to their new homes.'), - 'parser' => $subcommandParser - ))->addSubcommand('i18n', array( - 'help' => __d('cake_console', 'Update the i18n translation method calls.'), - 'parser' => $subcommandParser - ))->addSubcommand('helpers', array( - 'help' => __d('cake_console', 'Update calls to helpers.'), - 'parser' => $subcommandParser - ))->addSubcommand('basics', array( - 'help' => __d('cake_console', 'Update removed basics functions to PHP native functions.'), - 'parser' => $subcommandParser - ))->addSubcommand('request', array( - 'help' => __d('cake_console', 'Update removed request access, and replace with $this->request.'), - 'parser' => $subcommandParser - ))->addSubcommand('configure', array( - 'help' => __d('cake_console', "Update Configure::read() to Configure::read('debug')"), - 'parser' => $subcommandParser - ))->addSubcommand('constants', array( - 'help' => __d('cake_console', "Replace Obsolete constants"), - 'parser' => $subcommandParser - ))->addSubcommand('controller_redirects', array( - 'help' => __d('cake_console', 'Return early on controller redirect calls.'), - 'parser' => $subcommandParser - ))->addSubcommand('components', array( - 'help' => __d('cake_console', 'Update components to extend Component class.'), - 'parser' => $subcommandParser - ))->addSubcommand('exceptions', array( - 'help' => __d('cake_console', 'Replace use of cakeError with exceptions.'), - 'parser' => $subcommandParser - )); - - return $parser; - } +class UpgradeShell extends AppShell +{ + + /** + * Files + * + * @var array + */ + protected $_files = []; + + /** + * Paths + * + * @var array + */ + protected $_paths = []; + + /** + * Map + * + * @var array + */ + protected $_map = [ + 'Controller' => 'Controller', + 'Component' => 'Controller/Component', + 'Model' => 'Model', + 'Behavior' => 'Model/Behavior', + 'Datasource' => 'Model/Datasource', + 'Dbo' => 'Model/Datasource/Database', + 'View' => 'View', + 'Helper' => 'View/Helper', + 'Shell' => 'Console/Command', + 'Task' => 'Console/Command/Task', + 'Case' => 'Test/Case', + 'Fixture' => 'Test/Fixture', + 'Error' => 'Lib/Error', + ]; + + /** + * Shell startup, prints info message about dry run. + * + * @return void + */ + public function startup() + { + parent::startup(); + if ($this->params['dry-run']) { + $this->out(__d('cake_console', 'Dry-run mode enabled!'), 1, Shell::QUIET); + } + if ($this->params['git'] && !is_dir('.git')) { + $this->out(__d('cake_console', 'No git repository detected!'), 1, Shell::QUIET); + } + } + + /** + * Run all upgrade steps one at a time + * + * @return void + */ + public function all() + { + foreach ($this->OptionParser->subcommands() as $command) { + $name = $command->name(); + if ($name === 'all') { + continue; + } + $this->out(__d('cake_console', 'Running %s', $name)); + $this->$name(); + } + } + + /** + * Update tests. + * + * - Update tests class names to FooTest rather than FooTestCase. + * + * @return void + */ + public function tests() + { + $this->_paths = [APP . 'tests' . DS]; + if (!empty($this->params['plugin'])) { + $this->_paths = [CakePlugin::path($this->params['plugin']) . 'tests' . DS]; + } + $patterns = [ + [ + '*TestCase extends CakeTestCase to *Test extends CakeTestCase', + '/([a-zA-Z]*Test)Case extends CakeTestCase/', + '\1 extends CakeTestCase' + ], + ]; + + $this->_filesRegexpUpdate($patterns); + } + + /** + * Updates files based on regular expressions. + * + * @param array $patterns Array of search and replacement patterns. + * @return void + */ + protected function _filesRegexpUpdate($patterns) + { + $this->_findFiles($this->params['ext']); + foreach ($this->_files as $file) { + $this->out(__d('cake_console', 'Updating %s...', $file), 1, Shell::VERBOSE); + $this->_updateFile($file, $patterns); + } + } + + /** + * Searches the paths and finds files based on extension. + * + * @param string $extensions The extensions to include. Defaults to none. + * @return void + */ + protected function _findFiles($extensions = '') + { + $this->_files = []; + foreach ($this->_paths as $path) { + if (!is_dir($path)) { + continue; + } + $Iterator = new RegexIterator( + new RecursiveIteratorIterator(new RecursiveDirectoryIterator($path)), + '/^.+\.(' . $extensions . ')$/i', + RegexIterator::MATCH + ); + foreach ($Iterator as $file) { + if ($file->isFile()) { + $this->_files[] = $file->getPathname(); + } + } + } + } + + /** + * Update a single file. + * + * @param string $file The file to update + * @param array $patterns The replacement patterns to run. + * @return void + */ + protected function _updateFile($file, $patterns) + { + $contents = file_get_contents($file); + + foreach ($patterns as $pattern) { + $this->out(__d('cake_console', ' * Updating %s', $pattern[0]), 1, Shell::VERBOSE); + $contents = preg_replace($pattern[1], $pattern[2], $contents); + } + + $this->out(__d('cake_console', 'Done updating %s', $file), 1); + if (!$this->params['dry-run']) { + file_put_contents($file, $contents); + } + } + + /** + * Move files and folders to their new homes + * + * Moves folders containing files which cannot necessarily be auto-detected (libs and templates) + * and then looks for all php files except vendors, and moves them to where Cake 2.0 expects + * to find them. + * + * @return void + */ + public function locations() + { + $cwd = getcwd(); + + if (!empty($this->params['plugin'])) { + chdir(CakePlugin::path($this->params['plugin'])); + } + + if (is_dir('plugins')) { + $Folder = new Folder('plugins'); + list($plugins) = $Folder->read(); + foreach ($plugins as $plugin) { + chdir($cwd . DS . 'plugins' . DS . $plugin); + $this->out(__d('cake_console', 'Upgrading locations for plugin %s', $plugin)); + $this->locations(); + } + $this->_files = []; + chdir($cwd); + $this->out(__d('cake_console', 'Upgrading locations for app directory')); + } + $moves = [ + 'config' => 'Config', + 'Config' . DS . 'schema' => 'Config' . DS . 'Schema', + 'libs' => 'Lib', + 'tests' => 'Test', + 'views' => 'View', + 'models' => 'Model', + 'Model' . DS . 'behaviors' => 'Model' . DS . 'Behavior', + 'Model' . DS . 'datasources' => 'Model' . DS . 'Datasource', + 'Test' . DS . 'cases' => 'Test' . DS . 'Case', + 'Test' . DS . 'fixtures' => 'Test' . DS . 'Fixture', + 'vendors' . DS . 'shells' . DS . 'templates' => 'Console' . DS . 'Templates', + ]; + foreach ($moves as $old => $new) { + if (is_dir($old)) { + $this->out(__d('cake_console', 'Moving %s to %s', $old, $new)); + if (!$this->params['dry-run']) { + if ($this->params['git']) { + exec('git mv -f ' . escapeshellarg($old) . ' ' . escapeshellarg($old . '__')); + exec('git mv -f ' . escapeshellarg($old . '__') . ' ' . escapeshellarg($new)); + } else { + $Folder = new Folder($old); + $Folder->move($new); + } + } + } + } + + $this->_moveViewFiles(); + $this->_moveAppClasses(); + + $sourceDirs = [ + '.' => ['recursive' => false], + 'Console', + 'controllers', + 'Controller', + 'Lib' => ['checkFolder' => false], + 'models', + 'Model', + 'tests', + 'Test' => ['regex' => '@class (\S*Test) extends CakeTestCase@'], + 'views', + 'View', + 'vendors/shells', + ]; + + $defaultOptions = [ + 'recursive' => true, + 'checkFolder' => true, + 'regex' => '@class (\S*) .*(\s|\v)*{@i' + ]; + foreach ($sourceDirs as $dir => $options) { + if (is_numeric($dir)) { + $dir = $options; + $options = []; + } + $options += $defaultOptions; + $this->_movePhpFiles($dir, $options); + } + } + + /** + * Move application views files to where they now should be + * + * Find all view files in the folder and determine where cake expects the file to be + * + * @return void + */ + protected function _moveViewFiles() + { + if (!is_dir('View')) { + return; + } + + $dirs = scandir('View'); + foreach ($dirs as $old) { + if (!is_dir('View' . DS . $old) || $old === '.' || $old === '..') { + continue; + } + + $new = 'View' . DS . Inflector::camelize($old); + $old = 'View' . DS . $old; + if ($new === $old) { + continue; + } + + $this->out(__d('cake_console', 'Moving %s to %s', $old, $new)); + if (!$this->params['dry-run']) { + if ($this->params['git']) { + exec('git mv -f ' . escapeshellarg($old) . ' ' . escapeshellarg($old . '__')); + exec('git mv -f ' . escapeshellarg($old . '__') . ' ' . escapeshellarg($new)); + } else { + $Folder = new Folder($old); + $Folder->move($new); + } + } + } + } + + /** + * Move the AppController, and AppModel classes. + * + * @return void + */ + protected function _moveAppClasses() + { + $files = [ + APP . 'app_controller.php' => APP . 'Controller' . DS . 'AppController.php', + APP . 'controllers' . DS . 'app_controller.php' => APP . 'Controller' . DS . 'AppController.php', + APP . 'app_model.php' => APP . 'Model' . DS . 'AppModel.php', + APP . 'models' . DS . 'app_model.php' => APP . 'Model' . DS . 'AppModel.php', + ]; + foreach ($files as $old => $new) { + if (file_exists($old)) { + $this->out(__d('cake_console', 'Moving %s to %s', $old, $new)); + + if ($this->params['dry-run']) { + continue; + } + if ($this->params['git']) { + exec('git mv -f ' . escapeshellarg($old) . ' ' . escapeshellarg($old . '__')); + exec('git mv -f ' . escapeshellarg($old . '__') . ' ' . escapeshellarg($new)); + } else { + rename($old, $new); + } + } + } + } + + /** + * Move application php files to where they now should be + * + * Find all php files in the folder (honoring recursive) and determine where CakePHP expects the file to be + * If the file is not exactly where CakePHP expects it - move it. + * + * @param string $path The path to move files in. + * @param array $options array(recursive, checkFolder) + * @return void + */ + protected function _movePhpFiles($path, $options) + { + if (!is_dir($path)) { + return; + } + + $paths = $this->_paths; + + $this->_paths = [$path]; + $this->_files = []; + if ($options['recursive']) { + $this->_findFiles('php'); + } else { + $this->_files = scandir($path); + foreach ($this->_files as $i => $file) { + if (strlen($file) < 5 || substr($file, -4) !== '.php') { + unset($this->_files[$i]); + } + } + } + + $cwd = getcwd(); + foreach ($this->_files as &$file) { + $file = $cwd . DS . $file; + + $contents = file_get_contents($file); + preg_match($options['regex'], $contents, $match); + if (!$match) { + continue; + } + + $class = $match[1]; + + if (substr($class, 0, 3) === 'Dbo') { + $type = 'Dbo'; + } else { + preg_match('@([A-Z][^A-Z]*)$@', $class, $match); + if ($match) { + $type = $match[1]; + } else { + $type = 'unknown'; + } + } + + preg_match('@^.*[\\\/]plugins[\\\/](.*?)[\\\/]@', $file, $match); + $base = $cwd . DS; + $plugin = false; + if ($match) { + $base = $match[0]; + $plugin = $match[1]; + } + + if ($options['checkFolder'] && !empty($this->_map[$type])) { + $folder = str_replace('/', DS, $this->_map[$type]); + $new = $base . $folder . DS . $class . '.php'; + } else { + $new = dirname($file) . DS . $class . '.php'; + } + + if ($file === $new) { + continue; + } + + $dir = dirname($new); + if (!is_dir($dir)) { + new Folder($dir, true); + } + + $this->out(__d('cake_console', 'Moving %s to %s', $file, $new), 1, Shell::VERBOSE); + if (!$this->params['dry-run']) { + if ($this->params['git']) { + exec('git mv -f ' . escapeshellarg($file) . ' ' . escapeshellarg($file . '__')); + exec('git mv -f ' . escapeshellarg($file . '__') . ' ' . escapeshellarg($new)); + } else { + rename($file, $new); + } + } + } + + $this->_paths = $paths; + } + + /** + * Update helpers. + * + * - Converts helpers usage to new format. + * + * @return void + */ + public function helpers() + { + $this->_paths = array_diff(App::path('views'), App::core('views')); + + if (!empty($this->params['plugin'])) { + $this->_paths = [CakePlugin::path($this->params['plugin']) . 'views' . DS]; + } + + $patterns = []; + App::build([ + 'View/Helper' => App::core('View/Helper'), + ], App::APPEND); + $helpers = App::objects('helper'); + $plugins = App::objects('plugin'); + $pluginHelpers = []; + foreach ($plugins as $plugin) { + CakePlugin::load($plugin); + $pluginHelpers = array_merge( + $pluginHelpers, + App::objects('helper', CakePlugin::path($plugin) . DS . 'views' . DS . 'helpers' . DS, false) + ); + } + $helpers = array_merge($pluginHelpers, $helpers); + foreach ($helpers as $helper) { + $helper = preg_replace('/Helper$/', '', $helper); + $oldHelper = $helper; + $oldHelper{0} = strtolower($oldHelper{0}); + $patterns[] = [ + "\${$oldHelper} to \$this->{$helper}", + "/\\\${$oldHelper}->/", + "\\\$this->{$helper}->" + ]; + } + + $this->_filesRegexpUpdate($patterns); + } + + /** + * Update i18n. + * + * - Removes extra true param. + * - Add the echo to __*() calls that didn't need them before. + * + * @return void + */ + public function i18n() + { + $this->_paths = [ + APP + ]; + if (!empty($this->params['plugin'])) { + $this->_paths = [CakePlugin::path($this->params['plugin'])]; + } + + $patterns = [ + [ + '_filesRegexpUpdate($patterns); + } + + /** + * Upgrade the removed basics functions. + * + * - a(*) -> array(*) + * - e(*) -> echo * + * - ife(*, *, *) -> !empty(*) ? * : * + * - a(*) -> array(*) + * - r(*, *, *) -> str_replace(*, *, *) + * - up(*) -> strtoupper(*) + * - low(*, *, *) -> strtolower(*) + * - getMicrotime() -> microtime(true) + * + * @return void + */ + public function basics() + { + $this->_paths = [ + APP + ]; + if (!empty($this->params['plugin'])) { + $this->_paths = [CakePlugin::path($this->params['plugin'])]; + } + $patterns = [ + [ + 'a(*) -> array(*)', + '/\ba\((.*)\)/', + 'array(\1)' + ], + [ + 'e(*) -> echo *', + '/\be\((.*)\)/', + 'echo \1' + ], + [ + 'ife(*, *, *) -> !empty(*) ? * : *', + '/ife\((.*), (.*), (.*)\)/', + '!empty(\1) ? \2 : \3' + ], + [ + 'r(*, *, *) -> str_replace(*, *, *)', + '/\br\(/', + 'str_replace(' + ], + [ + 'up(*) -> strtoupper(*)', + '/\bup\(/', + 'strtoupper(' + ], + [ + 'low(*) -> strtolower(*)', + '/\blow\(/', + 'strtolower(' + ], + [ + 'getMicrotime() -> microtime(true)', + '/getMicrotime\(\)/', + 'microtime(true)' + ], + ]; + $this->_filesRegexpUpdate($patterns); + } + + /** + * Update the properties moved to CakeRequest. + * + * @return void + */ + public function request() + { + $views = array_diff(App::path('views'), App::core('views')); + $controllers = array_diff(App::path('controllers'), App::core('controllers'), [APP]); + $components = array_diff(App::path('components'), App::core('components')); + + $this->_paths = array_merge($views, $controllers, $components); + + if (!empty($this->params['plugin'])) { + $pluginPath = CakePlugin::path($this->params['plugin']); + $this->_paths = [ + $pluginPath . 'controllers' . DS, + $pluginPath . 'controllers' . DS . 'components' . DS, + $pluginPath . 'views' . DS, + ]; + } + $patterns = [ + [ + '$this->data -> $this->request->data', + '/(\$this->data\b(?!\())/', + '$this->request->data' + ], + [ + '$this->params -> $this->request->params', + '/(\$this->params\b(?!\())/', + '$this->request->params' + ], + [ + '$this->webroot -> $this->request->webroot', + '/(\$this->webroot\b(?!\())/', + '$this->request->webroot' + ], + [ + '$this->base -> $this->request->base', + '/(\$this->base\b(?!\())/', + '$this->request->base' + ], + [ + '$this->here -> $this->request->here', + '/(\$this->here\b(?!\())/', + '$this->request->here' + ], + [ + '$this->action -> $this->request->action', + '/(\$this->action\b(?!\())/', + '$this->request->action' + ], + [ + '$this->request->onlyAllow() -> $this->request->allowMethod()', + '/\$this->request->onlyAllow\(/', + '$this->request->allowMethod(' + ] + ]; + $this->_filesRegexpUpdate($patterns); + } + + /** + * Update Configure::read() calls with no params. + * + * @return void + */ + public function configure() + { + $this->_paths = [ + APP + ]; + if (!empty($this->params['plugin'])) { + $this->_paths = [CakePlugin::path($this->params['plugin'])]; + } + $patterns = [ + [ + "Configure::read() -> Configure::read('debug')", + '/Configure::read\(\)/', + 'Configure::read(\'debug\')' + ], + ]; + $this->_filesRegexpUpdate($patterns); + } + + /** + * constants + * + * @return void + */ + public function constants() + { + $this->_paths = [ + APP + ]; + if (!empty($this->params['plugin'])) { + $this->_paths = [CakePlugin::path($this->params['plugin'])]; + } + $patterns = [ + [ + "LIBS -> CAKE", + '/\bLIBS\b/', + 'CAKE' + ], + [ + "CONFIGS -> APP . 'Config' . DS", + '/\bCONFIGS\b/', + 'APP . \'Config\' . DS' + ], + [ + "CONTROLLERS -> APP . 'Controller' . DS", + '/\bCONTROLLERS\b/', + 'APP . \'Controller\' . DS' + ], + [ + "COMPONENTS -> APP . 'Controller' . DS . 'Component' . DS", + '/\bCOMPONENTS\b/', + 'APP . \'Controller\' . DS . \'Component\'' + ], + [ + "MODELS -> APP . 'Model' . DS", + '/\bMODELS\b/', + 'APP . \'Model\' . DS' + ], + [ + "BEHAVIORS -> APP . 'Model' . DS . 'Behavior' . DS", + '/\bBEHAVIORS\b/', + 'APP . \'Model\' . DS . \'Behavior\' . DS' + ], + [ + "VIEWS -> APP . 'View' . DS", + '/\bVIEWS\b/', + 'APP . \'View\' . DS' + ], + [ + "HELPERS -> APP . 'View' . DS . 'Helper' . DS", + '/\bHELPERS\b/', + 'APP . \'View\' . DS . \'Helper\' . DS' + ], + [ + "LAYOUTS -> APP . 'View' . DS . 'Layouts' . DS", + '/\bLAYOUTS\b/', + 'APP . \'View\' . DS . \'Layouts\' . DS' + ], + [ + "ELEMENTS -> APP . 'View' . DS . 'Elements' . DS", + '/\bELEMENTS\b/', + 'APP . \'View\' . DS . \'Elements\' . DS' + ], + [ + "CONSOLE_LIBS -> CAKE . 'Console' . DS", + '/\bCONSOLE_LIBS\b/', + 'CAKE . \'Console\' . DS' + ], + [ + "CAKE_TESTS_LIB -> CAKE . 'TestSuite' . DS", + '/\bCAKE_TESTS_LIB\b/', + 'CAKE . \'TestSuite\' . DS' + ], + [ + "CAKE_TESTS -> CAKE . 'Test' . DS", + '/\bCAKE_TESTS\b/', + 'CAKE . \'Test\' . DS' + ] + ]; + $this->_filesRegexpUpdate($patterns); + } + + /** + * Update controller redirects. + * + * - Make redirect statements return early. + * + * @return void + */ + public function controller_redirects() + { + $this->_paths = App::Path('Controller'); + if (!empty($this->params['plugin'])) { + $this->_paths = App::Path('Controller', $this->params['plugin']); + } + $patterns = [ + [ + '$this->redirect() to return $this->redirect()', + '/\t\$this-\>redirect\(/', + "\t" . 'return $this->redirect(' + ], + ]; + + $this->_filesRegexpUpdate($patterns); + } + + /** + * Update components. + * + * - Make components that extend CakeObject to extend Component. + * + * @return void + */ + public function components() + { + $this->_paths = App::Path('Controller/Component'); + if (!empty($this->params['plugin'])) { + $this->_paths = App::Path('Controller/Component', $this->params['plugin']); + } + $patterns = [ + [ + '*Component extends Object to *Component extends Component', + '/([a-zA-Z]*Component extends) Object/', + '\1 Component' + ], + [ + '*Component extends CakeObject to *Component extends Component', + '/([a-zA-Z]*Component extends) CakeObject/', + '\1 Component' + ], + ]; + + $this->_filesRegexpUpdate($patterns); + } + + /** + * Replace cakeError with built-in exceptions. + * NOTE: this ignores calls where you've passed your own secondary parameters to cakeError(). + * + * @return void + */ + public function exceptions() + { + $controllers = array_diff(App::path('controllers'), App::core('controllers'), [APP]); + $components = array_diff(App::path('components'), App::core('components')); + + $this->_paths = array_merge($controllers, $components); + + if (!empty($this->params['plugin'])) { + $pluginPath = CakePlugin::path($this->params['plugin']); + $this->_paths = [ + $pluginPath . 'controllers' . DS, + $pluginPath . 'controllers' . DS . 'components' . DS, + ]; + } + $patterns = [ + [ + '$this->cakeError("error400") -> throw new BadRequestException()', + '/(\$this->cakeError\(["\']error400["\']\));/', + 'throw new BadRequestException();' + ], + [ + '$this->cakeError("error404") -> throw new NotFoundException()', + '/(\$this->cakeError\(["\']error404["\']\));/', + 'throw new NotFoundException();' + ], + [ + '$this->cakeError("error500") -> throw new InternalErrorException()', + '/(\$this->cakeError\(["\']error500["\']\));/', + 'throw new InternalErrorException();' + ], + ]; + $this->_filesRegexpUpdate($patterns); + } + + /** + * Gets the option parser instance and configures it. + * + * @return ConsoleOptionParser + */ + public function getOptionParser() + { + $parser = parent::getOptionParser(); + + $subcommandParser = [ + 'options' => [ + 'plugin' => [ + 'short' => 'p', + 'help' => __d('cake_console', 'The plugin to update. Only the specified plugin will be updated.') + ], + 'ext' => [ + 'short' => 'e', + 'help' => __d('cake_console', 'The extension(s) to search. A pipe delimited list, or a preg_match compatible subpattern'), + 'default' => 'php|ctp|thtml|inc|tpl' + ], + 'git' => [ + 'short' => 'g', + 'help' => __d('cake_console', 'Use git command for moving files around.'), + 'boolean' => true + ], + 'dry-run' => [ + 'short' => 'd', + 'help' => __d('cake_console', 'Dry run the update, no files will actually be modified.'), + 'boolean' => true + ] + ] + ]; + + $parser->description( + __d('cake_console', "A tool to help automate upgrading an application or plugin " . + "from CakePHP 1.3 to 2.0. Be sure to have a backup of your application before " . + "running these commands." + ))->addSubcommand('all', [ + 'help' => __d('cake_console', 'Run all upgrade commands.'), + 'parser' => $subcommandParser + ])->addSubcommand('tests', [ + 'help' => __d('cake_console', 'Update tests class names to FooTest rather than FooTestCase.'), + 'parser' => $subcommandParser + ])->addSubcommand('locations', [ + 'help' => __d('cake_console', 'Move files and folders to their new homes.'), + 'parser' => $subcommandParser + ])->addSubcommand('i18n', [ + 'help' => __d('cake_console', 'Update the i18n translation method calls.'), + 'parser' => $subcommandParser + ])->addSubcommand('helpers', [ + 'help' => __d('cake_console', 'Update calls to helpers.'), + 'parser' => $subcommandParser + ])->addSubcommand('basics', [ + 'help' => __d('cake_console', 'Update removed basics functions to PHP native functions.'), + 'parser' => $subcommandParser + ])->addSubcommand('request', [ + 'help' => __d('cake_console', 'Update removed request access, and replace with $this->request.'), + 'parser' => $subcommandParser + ])->addSubcommand('configure', [ + 'help' => __d('cake_console', "Update Configure::read() to Configure::read('debug')"), + 'parser' => $subcommandParser + ])->addSubcommand('constants', [ + 'help' => __d('cake_console', "Replace Obsolete constants"), + 'parser' => $subcommandParser + ])->addSubcommand('controller_redirects', [ + 'help' => __d('cake_console', 'Return early on controller redirect calls.'), + 'parser' => $subcommandParser + ])->addSubcommand('components', [ + 'help' => __d('cake_console', 'Update components to extend Component class.'), + 'parser' => $subcommandParser + ])->addSubcommand('exceptions', [ + 'help' => __d('cake_console', 'Replace use of cakeError with exceptions.'), + 'parser' => $subcommandParser + ]); + + return $parser; + } } diff --git a/lib/Cake/Console/ConsoleErrorHandler.php b/lib/Cake/Console/ConsoleErrorHandler.php index 3df0bf73..9a88d28f 100755 --- a/lib/Cake/Console/ConsoleErrorHandler.php +++ b/lib/Cake/Console/ConsoleErrorHandler.php @@ -25,81 +25,86 @@ * * @package Cake.Console */ -class ConsoleErrorHandler { +class ConsoleErrorHandler +{ -/** - * Standard error stream. - * - * @var ConsoleOutput - */ - public static $stderr; + /** + * Standard error stream. + * + * @var ConsoleOutput + */ + public static $stderr; -/** - * Get the stderr object for the console error handling. - * - * @return ConsoleOutput - */ - public static function getStderr() { - if (empty(static::$stderr)) { - static::$stderr = new ConsoleOutput('php://stderr'); - } - return static::$stderr; - } + /** + * Handle an exception in the console environment. Prints a message to stderr. + * + * @param Exception|ParserError $exception The exception to handle + * @return void + */ + public function handleException($exception) + { + $stderr = static::getStderr(); + $stderr->write(__d('cake_console', "Error: %s\n%s", + $exception->getMessage(), + $exception->getTraceAsString() + )); + $code = $exception->getCode(); + $code = ($code && is_int($code)) ? $code : 1; + return $this->_stop($code); + } -/** - * Handle an exception in the console environment. Prints a message to stderr. - * - * @param Exception|ParserError $exception The exception to handle - * @return void - */ - public function handleException($exception) { - $stderr = static::getStderr(); - $stderr->write(__d('cake_console', "Error: %s\n%s", - $exception->getMessage(), - $exception->getTraceAsString() - )); - $code = $exception->getCode(); - $code = ($code && is_int($code)) ? $code : 1; - return $this->_stop($code); - } + /** + * Get the stderr object for the console error handling. + * + * @return ConsoleOutput + */ + public static function getStderr() + { + if (empty(static::$stderr)) { + static::$stderr = new ConsoleOutput('php://stderr'); + } + return static::$stderr; + } -/** - * Handle errors in the console environment. Writes errors to stderr, - * and logs messages if Configure::read('debug') is 0. - * - * @param int $code Error code - * @param string $description Description of the error. - * @param string $file The file the error occurred in. - * @param int $line The line the error occurred on. - * @param array $context The backtrace of the error. - * @return void - */ - public function handleError($code, $description, $file = null, $line = null, $context = null) { - if (error_reporting() === 0) { - return; - } - $stderr = static::getStderr(); - list($name, $log) = ErrorHandler::mapErrorCode($code); - $message = __d('cake_console', '%s in [%s, line %s]', $description, $file, $line); - $stderr->write(__d('cake_console', "%s Error: %s\n", $name, $message)); + /** + * Wrapper for exit(), used for testing. + * + * @param int $code The exit code. + * @return void + */ + protected function _stop($code = 0) + { + exit($code); + } - if (!Configure::read('debug')) { - CakeLog::write($log, $message); - } + /** + * Handle errors in the console environment. Writes errors to stderr, + * and logs messages if Configure::read('debug') is 0. + * + * @param int $code Error code + * @param string $description Description of the error. + * @param string $file The file the error occurred in. + * @param int $line The line the error occurred on. + * @param array $context The backtrace of the error. + * @return void + */ + public function handleError($code, $description, $file = null, $line = null, $context = null) + { + if (error_reporting() === 0) { + return; + } + $stderr = static::getStderr(); + list($name, $log) = ErrorHandler::mapErrorCode($code); + $message = __d('cake_console', '%s in [%s, line %s]', $description, $file, $line); + $stderr->write(__d('cake_console', "%s Error: %s\n", $name, $message)); - if ($log === LOG_ERR) { - return $this->_stop(1); - } - } + if (!Configure::read('debug')) { + CakeLog::write($log, $message); + } -/** - * Wrapper for exit(), used for testing. - * - * @param int $code The exit code. - * @return void - */ - protected function _stop($code = 0) { - exit($code); - } + if ($log === LOG_ERR) { + return $this->_stop(1); + } + } } diff --git a/lib/Cake/Console/ConsoleInput.php b/lib/Cake/Console/ConsoleInput.php index dc416ce8..49371a55 100755 --- a/lib/Cake/Console/ConsoleInput.php +++ b/lib/Cake/Console/ConsoleInput.php @@ -21,62 +21,66 @@ * * @package Cake.Console */ -class ConsoleInput { +class ConsoleInput +{ -/** - * Input value. - * - * @var resource - */ - protected $_input; + /** + * Input value. + * + * @var resource + */ + protected $_input; -/** - * Can this instance use readline? - * Two conditions must be met: - * 1. Readline support must be enabled. - * 2. Handle we are attached to must be stdin. - * Allows rich editing with arrow keys and history when inputting a string. - * - * @var bool - */ - protected $_canReadline; + /** + * Can this instance use readline? + * Two conditions must be met: + * 1. Readline support must be enabled. + * 2. Handle we are attached to must be stdin. + * Allows rich editing with arrow keys and history when inputting a string. + * + * @var bool + */ + protected $_canReadline; -/** - * Constructor - * - * @param string $handle The location of the stream to use as input. - */ - public function __construct($handle = 'php://stdin') { - $this->_canReadline = extension_loaded('readline') && $handle === 'php://stdin' ? true : false; - $this->_input = fopen($handle, 'r'); - } + /** + * Constructor + * + * @param string $handle The location of the stream to use as input. + */ + public function __construct($handle = 'php://stdin') + { + $this->_canReadline = extension_loaded('readline') && $handle === 'php://stdin' ? true : false; + $this->_input = fopen($handle, 'r'); + } -/** - * Read a value from the stream - * - * @return mixed The value of the stream - */ - public function read() { - if ($this->_canReadline) { - $line = readline(''); - if (!empty($line)) { - readline_add_history($line); - } - return $line; - } - return fgets($this->_input); - } + /** + * Read a value from the stream + * + * @return mixed The value of the stream + */ + public function read() + { + if ($this->_canReadline) { + $line = readline(''); + if (!empty($line)) { + readline_add_history($line); + } + return $line; + } + return fgets($this->_input); + } -/** - * Checks if data is available on the stream - * - * @param int $timeout An optional time to wait for data - * @return bool True for data available, false otherwise - */ - public function dataAvailable($timeout = 0) { - $readFds = array($this->_input); - $readyFds = stream_select($readFds, $writeFds, $errorFds, $timeout); - return ($readyFds > 0); - } + /** + * Checks if data is available on the stream + * + * @param int $timeout An optional time to wait for data + * @return bool True for data available, false otherwise + */ + public function dataAvailable($timeout = 0) + { + $readFds = [$this->_input]; + $readyFds = stream_select($readFds, $writeFds, $errorFds, $timeout); + return ($readyFds > 0); + } } diff --git a/lib/Cake/Console/ConsoleInputArgument.php b/lib/Cake/Console/ConsoleInputArgument.php index 36f08550..fdb0740a 100755 --- a/lib/Cake/Console/ConsoleInputArgument.php +++ b/lib/Cake/Console/ConsoleInputArgument.php @@ -22,149 +22,157 @@ * @see ConsoleOptionParser::addArgument() * @package Cake.Console */ -class ConsoleInputArgument { +class ConsoleInputArgument +{ -/** - * Name of the argument. - * - * @var string - */ - protected $_name; + /** + * Name of the argument. + * + * @var string + */ + protected $_name; -/** - * Help string - * - * @var string - */ - protected $_help; + /** + * Help string + * + * @var string + */ + protected $_help; -/** - * Is this option required? - * - * @var bool - */ - protected $_required; + /** + * Is this option required? + * + * @var bool + */ + protected $_required; -/** - * An array of valid choices for this argument. - * - * @var array - */ - protected $_choices; + /** + * An array of valid choices for this argument. + * + * @var array + */ + protected $_choices; -/** - * Make a new Input Argument - * - * @param string|array $name The long name of the option, or an array with all the properties. - * @param string $help The help text for this option - * @param bool $required Whether this argument is required. Missing required args will trigger exceptions - * @param array $choices Valid choices for this option. - */ - public function __construct($name, $help = '', $required = false, $choices = array()) { - if (is_array($name) && isset($name['name'])) { - foreach ($name as $key => $value) { - $this->{'_' . $key} = $value; - } - } else { - $this->_name = $name; - $this->_help = $help; - $this->_required = $required; - $this->_choices = $choices; - } - } + /** + * Make a new Input Argument + * + * @param string|array $name The long name of the option, or an array with all the properties. + * @param string $help The help text for this option + * @param bool $required Whether this argument is required. Missing required args will trigger exceptions + * @param array $choices Valid choices for this option. + */ + public function __construct($name, $help = '', $required = false, $choices = []) + { + if (is_array($name) && isset($name['name'])) { + foreach ($name as $key => $value) { + $this->{'_' . $key} = $value; + } + } else { + $this->_name = $name; + $this->_help = $help; + $this->_required = $required; + $this->_choices = $choices; + } + } -/** - * Get the value of the name attribute. - * - * @return string Value of this->_name. - */ - public function name() { - return $this->_name; - } + /** + * Get the value of the name attribute. + * + * @return string Value of this->_name. + */ + public function name() + { + return $this->_name; + } -/** - * Generate the help for this argument. - * - * @param int $width The width to make the name of the option. - * @return string - */ - public function help($width = 0) { - $name = $this->_name; - if (strlen($name) < $width) { - $name = str_pad($name, $width, ' '); - } - $optional = ''; - if (!$this->isRequired()) { - $optional = __d('cake_console', ' (optional)'); - } - if (!empty($this->_choices)) { - $optional .= __d('cake_console', ' (choices: %s)', implode('|', $this->_choices)); - } - return sprintf('%s%s%s', $name, $this->_help, $optional); - } + /** + * Generate the help for this argument. + * + * @param int $width The width to make the name of the option. + * @return string + */ + public function help($width = 0) + { + $name = $this->_name; + if (strlen($name) < $width) { + $name = str_pad($name, $width, ' '); + } + $optional = ''; + if (!$this->isRequired()) { + $optional = __d('cake_console', ' (optional)'); + } + if (!empty($this->_choices)) { + $optional .= __d('cake_console', ' (choices: %s)', implode('|', $this->_choices)); + } + return sprintf('%s%s%s', $name, $this->_help, $optional); + } -/** - * Get the usage value for this argument - * - * @return string - */ - public function usage() { - $name = $this->_name; - if (!empty($this->_choices)) { - $name = implode('|', $this->_choices); - } - $name = '<' . $name . '>'; - if (!$this->isRequired()) { - $name = '[' . $name . ']'; - } - return $name; - } + /** + * Check if this argument is a required argument + * + * @return bool + */ + public function isRequired() + { + return (bool)$this->_required; + } -/** - * Check if this argument is a required argument - * - * @return bool - */ - public function isRequired() { - return (bool)$this->_required; - } + /** + * Get the usage value for this argument + * + * @return string + */ + public function usage() + { + $name = $this->_name; + if (!empty($this->_choices)) { + $name = implode('|', $this->_choices); + } + $name = '<' . $name . '>'; + if (!$this->isRequired()) { + $name = '[' . $name . ']'; + } + return $name; + } -/** - * Check that $value is a valid choice for this argument. - * - * @param string $value The choice to validate. - * @return bool - * @throws ConsoleException - */ - public function validChoice($value) { - if (empty($this->_choices)) { - return true; - } - if (!in_array($value, $this->_choices)) { - throw new ConsoleException( - __d('cake_console', '"%s" is not a valid value for %s. Please use one of "%s"', - $value, $this->_name, implode(', ', $this->_choices) - )); - } - return true; - } + /** + * Check that $value is a valid choice for this argument. + * + * @param string $value The choice to validate. + * @return bool + * @throws ConsoleException + */ + public function validChoice($value) + { + if (empty($this->_choices)) { + return true; + } + if (!in_array($value, $this->_choices)) { + throw new ConsoleException( + __d('cake_console', '"%s" is not a valid value for %s. Please use one of "%s"', + $value, $this->_name, implode(', ', $this->_choices) + )); + } + return true; + } -/** - * Append this arguments XML representation to the passed in SimpleXml object. - * - * @param SimpleXmlElement $parent The parent element. - * @return SimpleXmlElement The parent with this argument appended. - */ - public function xml(SimpleXmlElement $parent) { - $option = $parent->addChild('argument'); - $option->addAttribute('name', $this->_name); - $option->addAttribute('help', $this->_help); - $option->addAttribute('required', (int)$this->isRequired()); - $choices = $option->addChild('choices'); - foreach ($this->_choices as $valid) { - $choices->addChild('choice', $valid); - } - return $parent; - } + /** + * Append this arguments XML representation to the passed in SimpleXml object. + * + * @param SimpleXmlElement $parent The parent element. + * @return SimpleXmlElement The parent with this argument appended. + */ + public function xml(SimpleXmlElement $parent) + { + $option = $parent->addChild('argument'); + $option->addAttribute('name', $this->_name); + $option->addAttribute('help', $this->_help); + $option->addAttribute('required', (int)$this->isRequired()); + $choices = $option->addChild('choices'); + foreach ($this->_choices as $valid) { + $choices->addChild('choice', $valid); + } + return $parent; + } } diff --git a/lib/Cake/Console/ConsoleInputOption.php b/lib/Cake/Console/ConsoleInputOption.php index 139c9835..5a52fb91 100755 --- a/lib/Cake/Console/ConsoleInputOption.php +++ b/lib/Cake/Console/ConsoleInputOption.php @@ -22,200 +22,210 @@ * @see ConsoleOptionParser::addOption() * @package Cake.Console */ -class ConsoleInputOption { - -/** - * Name of the option - * - * @var string - */ - protected $_name; - -/** - * Short (1 character) alias for the option. - * - * @var string - */ - protected $_short; - -/** - * Help text for the option. - * - * @var string - */ - protected $_help; - -/** - * Is the option a boolean option. Boolean options do not consume a parameter. - * - * @var bool - */ - protected $_boolean; - -/** - * Default value for the option - * - * @var mixed - */ - protected $_default; - -/** - * An array of choices for the option. - * - * @var array - */ - protected $_choices; - -/** - * Make a new Input Option - * - * @param string|array $name The long name of the option, or an array with all the properties. - * @param string $short The short alias for this option - * @param string $help The help text for this option - * @param bool $boolean Whether this option is a boolean option. Boolean options don't consume extra tokens - * @param string $default The default value for this option. - * @param array $choices Valid choices for this option. - * @throws ConsoleException - */ - public function __construct($name, $short = null, $help = '', $boolean = false, $default = '', $choices = array()) { - if (is_array($name) && isset($name['name'])) { - foreach ($name as $key => $value) { - $this->{'_' . $key} = $value; - } - } else { - $this->_name = $name; - $this->_short = $short; - $this->_help = $help; - $this->_boolean = $boolean; - $this->_default = $default; - $this->_choices = $choices; - } - if (strlen($this->_short) > 1) { - throw new ConsoleException( - __d('cake_console', 'Short option "%s" is invalid, short options must be one letter.', $this->_short) - ); - } - } - -/** - * Get the value of the name attribute. - * - * @return string Value of this->_name. - */ - public function name() { - return $this->_name; - } - -/** - * Get the value of the short attribute. - * - * @return string Value of this->_short. - */ - public function short() { - return $this->_short; - } - -/** - * Generate the help for this this option. - * - * @param int $width The width to make the name of the option. - * @return string - */ - public function help($width = 0) { - $default = $short = ''; - if (!empty($this->_default) && $this->_default !== true) { - $default = __d('cake_console', ' (default: %s)', $this->_default); - } - if (!empty($this->_choices)) { - $default .= __d('cake_console', ' (choices: %s)', implode('|', $this->_choices)); - } - if (!empty($this->_short)) { - $short = ', -' . $this->_short; - } - $name = sprintf('--%s%s', $this->_name, $short); - if (strlen($name) < $width) { - $name = str_pad($name, $width, ' '); - } - return sprintf('%s%s%s', $name, $this->_help, $default); - } - -/** - * Get the usage value for this option - * - * @return string - */ - public function usage() { - $name = empty($this->_short) ? '--' . $this->_name : '-' . $this->_short; - $default = ''; - if (!empty($this->_default) && $this->_default !== true) { - $default = ' ' . $this->_default; - } - if (!empty($this->_choices)) { - $default = ' ' . implode('|', $this->_choices); - } - return sprintf('[%s%s]', $name, $default); - } - -/** - * Get the default value for this option - * - * @return mixed - */ - public function defaultValue() { - return $this->_default; - } - -/** - * Check if this option is a boolean option - * - * @return bool - */ - public function isBoolean() { - return (bool)$this->_boolean; - } - -/** - * Check that a value is a valid choice for this option. - * - * @param string $value The choice to validate. - * @return bool - * @throws ConsoleException - */ - public function validChoice($value) { - if (empty($this->_choices)) { - return true; - } - if (!in_array($value, $this->_choices)) { - throw new ConsoleException( - __d('cake_console', '"%s" is not a valid value for --%s. Please use one of "%s"', - $value, $this->_name, implode(', ', $this->_choices) - )); - } - return true; - } - -/** - * Append the option's xml into the parent. - * - * @param SimpleXmlElement $parent The parent element. - * @return SimpleXmlElement The parent with this option appended. - */ - public function xml(SimpleXmlElement $parent) { - $option = $parent->addChild('option'); - $option->addAttribute('name', '--' . $this->_name); - $short = ''; - if (strlen($this->_short)) { - $short = '-' . $this->_short; - } - $option->addAttribute('short', $short); - $option->addAttribute('help', $this->_help); - $option->addAttribute('boolean', (int)$this->_boolean); - $option->addChild('default', $this->_default); - $choices = $option->addChild('choices'); - foreach ($this->_choices as $valid) { - $choices->addChild('choice', $valid); - } - return $parent; - } +class ConsoleInputOption +{ + + /** + * Name of the option + * + * @var string + */ + protected $_name; + + /** + * Short (1 character) alias for the option. + * + * @var string + */ + protected $_short; + + /** + * Help text for the option. + * + * @var string + */ + protected $_help; + + /** + * Is the option a boolean option. Boolean options do not consume a parameter. + * + * @var bool + */ + protected $_boolean; + + /** + * Default value for the option + * + * @var mixed + */ + protected $_default; + + /** + * An array of choices for the option. + * + * @var array + */ + protected $_choices; + + /** + * Make a new Input Option + * + * @param string|array $name The long name of the option, or an array with all the properties. + * @param string $short The short alias for this option + * @param string $help The help text for this option + * @param bool $boolean Whether this option is a boolean option. Boolean options don't consume extra tokens + * @param string $default The default value for this option. + * @param array $choices Valid choices for this option. + * @throws ConsoleException + */ + public function __construct($name, $short = null, $help = '', $boolean = false, $default = '', $choices = []) + { + if (is_array($name) && isset($name['name'])) { + foreach ($name as $key => $value) { + $this->{'_' . $key} = $value; + } + } else { + $this->_name = $name; + $this->_short = $short; + $this->_help = $help; + $this->_boolean = $boolean; + $this->_default = $default; + $this->_choices = $choices; + } + if (strlen($this->_short) > 1) { + throw new ConsoleException( + __d('cake_console', 'Short option "%s" is invalid, short options must be one letter.', $this->_short) + ); + } + } + + /** + * Get the value of the name attribute. + * + * @return string Value of this->_name. + */ + public function name() + { + return $this->_name; + } + + /** + * Get the value of the short attribute. + * + * @return string Value of this->_short. + */ + public function short() + { + return $this->_short; + } + + /** + * Generate the help for this this option. + * + * @param int $width The width to make the name of the option. + * @return string + */ + public function help($width = 0) + { + $default = $short = ''; + if (!empty($this->_default) && $this->_default !== true) { + $default = __d('cake_console', ' (default: %s)', $this->_default); + } + if (!empty($this->_choices)) { + $default .= __d('cake_console', ' (choices: %s)', implode('|', $this->_choices)); + } + if (!empty($this->_short)) { + $short = ', -' . $this->_short; + } + $name = sprintf('--%s%s', $this->_name, $short); + if (strlen($name) < $width) { + $name = str_pad($name, $width, ' '); + } + return sprintf('%s%s%s', $name, $this->_help, $default); + } + + /** + * Get the usage value for this option + * + * @return string + */ + public function usage() + { + $name = empty($this->_short) ? '--' . $this->_name : '-' . $this->_short; + $default = ''; + if (!empty($this->_default) && $this->_default !== true) { + $default = ' ' . $this->_default; + } + if (!empty($this->_choices)) { + $default = ' ' . implode('|', $this->_choices); + } + return sprintf('[%s%s]', $name, $default); + } + + /** + * Get the default value for this option + * + * @return mixed + */ + public function defaultValue() + { + return $this->_default; + } + + /** + * Check if this option is a boolean option + * + * @return bool + */ + public function isBoolean() + { + return (bool)$this->_boolean; + } + + /** + * Check that a value is a valid choice for this option. + * + * @param string $value The choice to validate. + * @return bool + * @throws ConsoleException + */ + public function validChoice($value) + { + if (empty($this->_choices)) { + return true; + } + if (!in_array($value, $this->_choices)) { + throw new ConsoleException( + __d('cake_console', '"%s" is not a valid value for --%s. Please use one of "%s"', + $value, $this->_name, implode(', ', $this->_choices) + )); + } + return true; + } + + /** + * Append the option's xml into the parent. + * + * @param SimpleXmlElement $parent The parent element. + * @return SimpleXmlElement The parent with this option appended. + */ + public function xml(SimpleXmlElement $parent) + { + $option = $parent->addChild('option'); + $option->addAttribute('name', '--' . $this->_name); + $short = ''; + if (strlen($this->_short)) { + $short = '-' . $this->_short; + } + $option->addAttribute('short', $short); + $option->addAttribute('help', $this->_help); + $option->addAttribute('boolean', (int)$this->_boolean); + $option->addChild('default', $this->_default); + $choices = $option->addChild('choices'); + foreach ($this->_choices as $valid) { + $choices->addChild('choice', $valid); + } + return $parent; + } } diff --git a/lib/Cake/Console/ConsoleInputSubcommand.php b/lib/Cake/Console/ConsoleInputSubcommand.php index b6721069..47bb52c0 100755 --- a/lib/Cake/Console/ConsoleInputSubcommand.php +++ b/lib/Cake/Console/ConsoleInputSubcommand.php @@ -22,99 +22,105 @@ * @see ConsoleOptionParser::addSubcommand() * @package Cake.Console */ -class ConsoleInputSubcommand { +class ConsoleInputSubcommand +{ -/** - * Name of the subcommand - * - * @var string - */ - protected $_name; + /** + * Name of the subcommand + * + * @var string + */ + protected $_name; -/** - * Help string for the subcommand - * - * @var string - */ - protected $_help; + /** + * Help string for the subcommand + * + * @var string + */ + protected $_help; -/** - * The ConsoleOptionParser for this subcommand. - * - * @var ConsoleOptionParser - */ - protected $_parser; + /** + * The ConsoleOptionParser for this subcommand. + * + * @var ConsoleOptionParser + */ + protected $_parser; -/** - * Make a new Subcommand - * - * @param string|array $name The long name of the subcommand, or an array with all the properties. - * @param string $help The help text for this option - * @param ConsoleOptionParser|array $parser A parser for this subcommand. Either a ConsoleOptionParser, or an array that can be - * used with ConsoleOptionParser::buildFromArray() - */ - public function __construct($name, $help = '', $parser = null) { - if (is_array($name) && isset($name['name'])) { - foreach ($name as $key => $value) { - $this->{'_' . $key} = $value; - } - } else { - $this->_name = $name; - $this->_help = $help; - $this->_parser = $parser; - } - if (is_array($this->_parser)) { - $this->_parser['command'] = $this->_name; - $this->_parser = ConsoleOptionParser::buildFromArray($this->_parser); - } - } + /** + * Make a new Subcommand + * + * @param string|array $name The long name of the subcommand, or an array with all the properties. + * @param string $help The help text for this option + * @param ConsoleOptionParser|array $parser A parser for this subcommand. Either a ConsoleOptionParser, or an array that can be + * used with ConsoleOptionParser::buildFromArray() + */ + public function __construct($name, $help = '', $parser = null) + { + if (is_array($name) && isset($name['name'])) { + foreach ($name as $key => $value) { + $this->{'_' . $key} = $value; + } + } else { + $this->_name = $name; + $this->_help = $help; + $this->_parser = $parser; + } + if (is_array($this->_parser)) { + $this->_parser['command'] = $this->_name; + $this->_parser = ConsoleOptionParser::buildFromArray($this->_parser); + } + } -/** - * Get the value of the name attribute. - * - * @return string Value of this->_name. - */ - public function name() { - return $this->_name; - } + /** + * Get the value of the name attribute. + * + * @return string Value of this->_name. + */ + public function name() + { + return $this->_name; + } -/** - * Generate the help for this this subcommand. - * - * @param int $width The width to make the name of the subcommand. - * @return string - */ - public function help($width = 0) { - $name = $this->_name; - if (strlen($name) < $width) { - $name = str_pad($name, $width, ' '); - } - return $name . $this->_help; - } + /** + * Generate the help for this this subcommand. + * + * @param int $width The width to make the name of the subcommand. + * @return string + */ + public function help($width = 0) + { + $name = $this->_name; + if (strlen($name) < $width) { + $name = str_pad($name, $width, ' '); + } + return $name . $this->_help; + } -/** - * Get the usage value for this option - * - * @return mixed Either false or a ConsoleOptionParser - */ - public function parser() { - if ($this->_parser instanceof ConsoleOptionParser) { - return $this->_parser; - } - return false; - } + /** + * Get the usage value for this option + * + * @return mixed Either false or a ConsoleOptionParser + */ + public function parser() + { + if ($this->_parser instanceof ConsoleOptionParser) { + return $this->_parser; + } + return false; + } -/** - * Append this subcommand to the Parent element - * - * @param SimpleXmlElement $parent The parent element. - * @return SimpleXmlElement The parent with this subcommand appended. - */ - public function xml(SimpleXmlElement $parent) { - $command = $parent->addChild('command'); - $command->addAttribute('name', $this->_name); - $command->addAttribute('help', $this->_help); - return $parent; - } + /** + * Append this subcommand to the Parent element + * + * @param SimpleXmlElement $parent The parent element. + * @return SimpleXmlElement The parent with this subcommand appended. + */ + public function xml(SimpleXmlElement $parent) + { + $command = $parent->addChild('command'); + $command->addAttribute('name', $this->_name); + $command->addAttribute('help', $this->_help); + return $parent; + } } diff --git a/lib/Cake/Console/ConsoleOptionParser.php b/lib/Cake/Console/ConsoleOptionParser.php index f90dbc4e..a45cddce 100755 --- a/lib/Cake/Console/ConsoleOptionParser.php +++ b/lib/Cake/Console/ConsoleOptionParser.php @@ -76,588 +76,613 @@ * * @package Cake.Console */ -class ConsoleOptionParser { - -/** - * Description text - displays before options when help is generated - * - * @see ConsoleOptionParser::description() - * @var string - */ - protected $_description = null; - -/** - * Epilog text - displays after options when help is generated - * - * @see ConsoleOptionParser::epilog() - * @var string - */ - protected $_epilog = null; - -/** - * Option definitions. - * - * @see ConsoleOptionParser::addOption() - * @var array - */ - protected $_options = array(); - -/** - * Map of short -> long options, generated when using addOption() - * - * @var string - */ - protected $_shortOptions = array(); - -/** - * Positional argument definitions. - * - * @see ConsoleOptionParser::addArgument() - * @var array - */ - protected $_args = array(); - -/** - * Subcommands for this Shell. - * - * @see ConsoleOptionParser::addSubcommand() - * @var array - */ - protected $_subcommands = array(); - -/** - * Command name. - * - * @var string - */ - protected $_command = ''; - -/** - * Construct an OptionParser so you can define its behavior - * - * @param string $command The command name this parser is for. The command name is used for generating help. - * @param bool $defaultOptions Whether you want the verbose and quiet options set. Setting - * this to false will prevent the addition of `--verbose` & `--quiet` options. - */ - public function __construct($command = null, $defaultOptions = true) { - $this->command($command); - - $this->addOption('help', array( - 'short' => 'h', - 'help' => __d('cake_console', 'Display this help.'), - 'boolean' => true - )); - - if ($defaultOptions) { - $this->addOption('verbose', array( - 'short' => 'v', - 'help' => __d('cake_console', 'Enable verbose output.'), - 'boolean' => true - ))->addOption('quiet', array( - 'short' => 'q', - 'help' => __d('cake_console', 'Enable quiet output.'), - 'boolean' => true - )); - } - } - -/** - * Static factory method for creating new OptionParsers so you can chain methods off of them. - * - * @param string $command The command name this parser is for. The command name is used for generating help. - * @param bool $defaultOptions Whether you want the verbose and quiet options set. - * @return ConsoleOptionParser - */ - public static function create($command, $defaultOptions = true) { - return new ConsoleOptionParser($command, $defaultOptions); - } - -/** - * Build a parser from an array. Uses an array like - * - * ``` - * $spec = array( - * 'description' => 'text', - * 'epilog' => 'text', - * 'arguments' => array( - * // list of arguments compatible with addArguments. - * ), - * 'options' => array( - * // list of options compatible with addOptions - * ), - * 'subcommands' => array( - * // list of subcommands to add. - * ) - * ); - * ``` - * - * @param array $spec The spec to build the OptionParser with. - * @return ConsoleOptionParser - */ - public static function buildFromArray($spec) { - $parser = new ConsoleOptionParser($spec['command']); - if (!empty($spec['arguments'])) { - $parser->addArguments($spec['arguments']); - } - if (!empty($spec['options'])) { - $parser->addOptions($spec['options']); - } - if (!empty($spec['subcommands'])) { - $parser->addSubcommands($spec['subcommands']); - } - if (!empty($spec['description'])) { - $parser->description($spec['description']); - } - if (!empty($spec['epilog'])) { - $parser->epilog($spec['epilog']); - } - return $parser; - } - -/** - * Get or set the command name for shell/task. - * - * @param string $text The text to set, or null if you want to read - * @return string|self If reading, the value of the command. If setting $this will be returned. - */ - public function command($text = null) { - if ($text !== null) { - $this->_command = Inflector::underscore($text); - return $this; - } - return $this->_command; - } - -/** - * Get or set the description text for shell/task. - * - * @param string|array $text The text to set, or null if you want to read. If an array the - * text will be imploded with "\n" - * @return string|self If reading, the value of the description. If setting $this will be returned. - */ - public function description($text = null) { - if ($text !== null) { - if (is_array($text)) { - $text = implode("\n", $text); - } - $this->_description = $text; - return $this; - } - return $this->_description; - } - -/** - * Get or set an epilog to the parser. The epilog is added to the end of - * the options and arguments listing when help is generated. - * - * @param string|array $text Text when setting or null when reading. If an array the text will be imploded with "\n" - * @return string|self If reading, the value of the epilog. If setting $this will be returned. - */ - public function epilog($text = null) { - if ($text !== null) { - if (is_array($text)) { - $text = implode("\n", $text); - } - $this->_epilog = $text; - return $this; - } - return $this->_epilog; - } - -/** - * Add an option to the option parser. Options allow you to define optional or required - * parameters for your console application. Options are defined by the parameters they use. - * - * ### Options - * - * - `short` - The single letter variant for this option, leave undefined for none. - * - `help` - Help text for this option. Used when generating help for the option. - * - `default` - The default value for this option. Defaults are added into the parsed params when the - * attached option is not provided or has no value. Using default and boolean together will not work. - * are added into the parsed parameters when the option is undefined. Defaults to null. - * - `boolean` - The option uses no value, its just a boolean switch. Defaults to false. - * If an option is defined as boolean, it will always be added to the parsed params. If no present - * it will be false, if present it will be true. - * - `choices` A list of valid choices for this option. If left empty all values are valid.. - * An exception will be raised when parse() encounters an invalid value. - * - * @param ConsoleInputOption|string $name The long name you want to the value to be parsed out as when options are parsed. - * Will also accept an instance of ConsoleInputOption - * @param array $options An array of parameters that define the behavior of the option - * @return self - */ - public function addOption($name, $options = array()) { - if (is_object($name) && $name instanceof ConsoleInputOption) { - $option = $name; - $name = $option->name(); - } else { - $defaults = array( - 'name' => $name, - 'short' => null, - 'help' => '', - 'default' => null, - 'boolean' => false, - 'choices' => array() - ); - $options += $defaults; - $option = new ConsoleInputOption($options); - } - $this->_options[$name] = $option; - if ($option->short() !== null) { - $this->_shortOptions[$option->short()] = $name; - } - return $this; - } - -/** - * Add a positional argument to the option parser. - * - * ### Params - * - * - `help` The help text to display for this argument. - * - `required` Whether this parameter is required. - * - `index` The index for the arg, if left undefined the argument will be put - * onto the end of the arguments. If you define the same index twice the first - * option will be overwritten. - * - `choices` A list of valid choices for this argument. If left empty all values are valid.. - * An exception will be raised when parse() encounters an invalid value. - * - * @param ConsoleInputArgument|string $name The name of the argument. Will also accept an instance of ConsoleInputArgument - * @param array $params Parameters for the argument, see above. - * @return self - */ - public function addArgument($name, $params = array()) { - if (is_object($name) && $name instanceof ConsoleInputArgument) { - $arg = $name; - $index = count($this->_args); - } else { - $defaults = array( - 'name' => $name, - 'help' => '', - 'index' => count($this->_args), - 'required' => false, - 'choices' => array() - ); - $options = $params + $defaults; - $index = $options['index']; - unset($options['index']); - $arg = new ConsoleInputArgument($options); - } - $this->_args[$index] = $arg; - ksort($this->_args); - return $this; - } - -/** - * Add multiple arguments at once. Take an array of argument definitions. - * The keys are used as the argument names, and the values as params for the argument. - * - * @param array $args Array of arguments to add. - * @see ConsoleOptionParser::addArgument() - * @return self - */ - public function addArguments(array $args) { - foreach ($args as $name => $params) { - $this->addArgument($name, $params); - } - return $this; - } - -/** - * Add multiple options at once. Takes an array of option definitions. - * The keys are used as option names, and the values as params for the option. - * - * @param array $options Array of options to add. - * @see ConsoleOptionParser::addOption() - * @return self - */ - public function addOptions(array $options) { - foreach ($options as $name => $params) { - $this->addOption($name, $params); - } - return $this; - } - -/** - * Append a subcommand to the subcommand list. - * Subcommands are usually methods on your Shell, but can also be used to document Tasks. - * - * ### Options - * - * - `help` - Help text for the subcommand. - * - `parser` - A ConsoleOptionParser for the subcommand. This allows you to create method - * specific option parsers. When help is generated for a subcommand, if a parser is present - * it will be used. - * - * @param ConsoleInputSubcommand|string $name Name of the subcommand. Will also accept an instance of ConsoleInputSubcommand - * @param array $options Array of params, see above. - * @return self - */ - public function addSubcommand($name, $options = array()) { - if (is_object($name) && $name instanceof ConsoleInputSubcommand) { - $command = $name; - $name = $command->name(); - } else { - $defaults = array( - 'name' => $name, - 'help' => '', - 'parser' => null - ); - $options += $defaults; - $command = new ConsoleInputSubcommand($options); - } - $this->_subcommands[$name] = $command; - return $this; - } - -/** - * Remove a subcommand from the option parser. - * - * @param string $name The subcommand name to remove. - * @return self - */ - public function removeSubcommand($name) { - unset($this->_subcommands[$name]); - return $this; - } - -/** - * Add multiple subcommands at once. - * - * @param array $commands Array of subcommands. - * @return self - */ - public function addSubcommands(array $commands) { - foreach ($commands as $name => $params) { - $this->addSubcommand($name, $params); - } - return $this; - } - -/** - * Gets the arguments defined in the parser. - * - * @return array Array of argument descriptions - */ - public function arguments() { - return $this->_args; - } - -/** - * Get the defined options in the parser. - * - * @return array - */ - public function options() { - return $this->_options; - } - -/** - * Get the array of defined subcommands - * - * @return array - */ - public function subcommands() { - return $this->_subcommands; - } - -/** - * Parse the argv array into a set of params and args. If $command is not null - * and $command is equal to a subcommand that has a parser, that parser will be used - * to parse the $argv - * - * @param array $argv Array of args (argv) to parse. - * @param string $command The subcommand to use. If this parameter is a subcommand, that has a parser, - * That parser will be used to parse $argv instead. - * @return array array($params, $args) - * @throws ConsoleException When an invalid parameter is encountered. - */ - public function parse($argv, $command = null) { - if (isset($this->_subcommands[$command]) && $this->_subcommands[$command]->parser()) { - return $this->_subcommands[$command]->parser()->parse($argv); - } - $params = $args = array(); - $this->_tokens = $argv; - while (($token = array_shift($this->_tokens)) !== null) { - if (substr($token, 0, 2) === '--') { - $params = $this->_parseLongOption($token, $params); - } elseif (substr($token, 0, 1) === '-') { - $params = $this->_parseShortOption($token, $params); - } else { - $args = $this->_parseArg($token, $args); - } - } - foreach ($this->_args as $i => $arg) { - if ($arg->isRequired() && !isset($args[$i]) && empty($params['help'])) { - throw new ConsoleException( - __d('cake_console', 'Missing required arguments. %s is required.', $arg->name()) - ); - } - } - foreach ($this->_options as $option) { - $name = $option->name(); - $isBoolean = $option->isBoolean(); - $default = $option->defaultValue(); - - if ($default !== null && !isset($params[$name]) && !$isBoolean) { - $params[$name] = $default; - } - if ($isBoolean && !isset($params[$name])) { - $params[$name] = false; - } - } - return array($params, $args); - } - -/** - * Gets formatted help for this parser object. - * Generates help text based on the description, options, arguments, subcommands and epilog - * in the parser. - * - * @param string $subcommand If present and a valid subcommand that has a linked parser. - * That subcommands help will be shown instead. - * @param string $format Define the output format, can be text or xml - * @param int $width The width to format user content to. Defaults to 72 - * @return string Generated help. - */ - public function help($subcommand = null, $format = 'text', $width = 72) { - if (isset($this->_subcommands[$subcommand]) && - $this->_subcommands[$subcommand]->parser() instanceof self - ) { - $subparser = $this->_subcommands[$subcommand]->parser(); - $subparser->command($this->command() . ' ' . $subparser->command()); - return $subparser->help(null, $format, $width); - } - $formatter = new HelpFormatter($this); - if ($format === 'text' || $format === true) { - return $formatter->text($width); - } elseif ($format === 'xml') { - return $formatter->xml(); - } - } - -/** - * Parse the value for a long option out of $this->_tokens. Will handle - * options with an `=` in them. - * - * @param string $option The option to parse. - * @param array $params The params to append the parsed value into - * @return array Params with $option added in. - */ - protected function _parseLongOption($option, $params) { - $name = substr($option, 2); - if (strpos($name, '=') !== false) { - list($name, $value) = explode('=', $name, 2); - array_unshift($this->_tokens, $value); - } - return $this->_parseOption($name, $params); - } - -/** - * Parse the value for a short option out of $this->_tokens - * If the $option is a combination of multiple shortcuts like -otf - * they will be shifted onto the token stack and parsed individually. - * - * @param string $option The option to parse. - * @param array $params The params to append the parsed value into - * @return array Params with $option added in. - * @throws ConsoleException When unknown short options are encountered. - */ - protected function _parseShortOption($option, $params) { - $key = substr($option, 1); - if (strlen($key) > 1) { - $flags = str_split($key); - $key = $flags[0]; - for ($i = 1, $len = count($flags); $i < $len; $i++) { - array_unshift($this->_tokens, '-' . $flags[$i]); - } - } - if (!isset($this->_shortOptions[$key])) { - throw new ConsoleException(__d('cake_console', 'Unknown short option `%s`', $key)); - } - $name = $this->_shortOptions[$key]; - return $this->_parseOption($name, $params); - } - -/** - * Parse an option by its name index. - * - * @param string $name The name to parse. - * @param array $params The params to append the parsed value into - * @return array Params with $option added in. - * @throws ConsoleException - */ - protected function _parseOption($name, $params) { - if (!isset($this->_options[$name])) { - throw new ConsoleException(__d('cake_console', 'Unknown option `%s`', $name)); - } - $option = $this->_options[$name]; - $isBoolean = $option->isBoolean(); - $nextValue = $this->_nextToken(); - $emptyNextValue = (empty($nextValue) && $nextValue !== '0'); - if (!$isBoolean && !$emptyNextValue && !$this->_optionExists($nextValue)) { - array_shift($this->_tokens); - $value = $nextValue; - } elseif ($isBoolean) { - $value = true; - } else { - $value = $option->defaultValue(); - } - if ($option->validChoice($value)) { - $params[$name] = $value; - return $params; - } - return array(); - } - -/** - * Check to see if $name has an option (short/long) defined for it. - * - * @param string $name The name of the option. - * @return bool - */ - protected function _optionExists($name) { - if (substr($name, 0, 2) === '--') { - return isset($this->_options[substr($name, 2)]); - } - if ($name{0} === '-' && $name{1} !== '-') { - return isset($this->_shortOptions[$name{1}]); - } - return false; - } - -/** - * Parse an argument, and ensure that the argument doesn't exceed the number of arguments - * and that the argument is a valid choice. - * - * @param string $argument The argument to append - * @param array $args The array of parsed args to append to. - * @return array Args - * @throws ConsoleException - */ - protected function _parseArg($argument, $args) { - if (empty($this->_args)) { - $args[] = $argument; - return $args; - } - $next = count($args); - if (!isset($this->_args[$next])) { - throw new ConsoleException(__d('cake_console', 'Too many arguments.')); - } - - if ($this->_args[$next]->validChoice($argument)) { - $args[] = $argument; - return $args; - } - } - -/** - * Find the next token in the argv set. - * - * @return string next token or '' - */ - protected function _nextToken() { - return isset($this->_tokens[0]) ? $this->_tokens[0] : ''; - } +class ConsoleOptionParser +{ + + /** + * Description text - displays before options when help is generated + * + * @see ConsoleOptionParser::description() + * @var string + */ + protected $_description = null; + + /** + * Epilog text - displays after options when help is generated + * + * @see ConsoleOptionParser::epilog() + * @var string + */ + protected $_epilog = null; + + /** + * Option definitions. + * + * @see ConsoleOptionParser::addOption() + * @var array + */ + protected $_options = []; + + /** + * Map of short -> long options, generated when using addOption() + * + * @var string + */ + protected $_shortOptions = []; + + /** + * Positional argument definitions. + * + * @see ConsoleOptionParser::addArgument() + * @var array + */ + protected $_args = []; + + /** + * Subcommands for this Shell. + * + * @see ConsoleOptionParser::addSubcommand() + * @var array + */ + protected $_subcommands = []; + + /** + * Command name. + * + * @var string + */ + protected $_command = ''; + + /** + * Construct an OptionParser so you can define its behavior + * + * @param string $command The command name this parser is for. The command name is used for generating help. + * @param bool $defaultOptions Whether you want the verbose and quiet options set. Setting + * this to false will prevent the addition of `--verbose` & `--quiet` options. + */ + public function __construct($command = null, $defaultOptions = true) + { + $this->command($command); + + $this->addOption('help', [ + 'short' => 'h', + 'help' => __d('cake_console', 'Display this help.'), + 'boolean' => true + ]); + + if ($defaultOptions) { + $this->addOption('verbose', [ + 'short' => 'v', + 'help' => __d('cake_console', 'Enable verbose output.'), + 'boolean' => true + ])->addOption('quiet', [ + 'short' => 'q', + 'help' => __d('cake_console', 'Enable quiet output.'), + 'boolean' => true + ]); + } + } + + /** + * Get or set the command name for shell/task. + * + * @param string $text The text to set, or null if you want to read + * @return string|self If reading, the value of the command. If setting $this will be returned. + */ + public function command($text = null) + { + if ($text !== null) { + $this->_command = Inflector::underscore($text); + return $this; + } + return $this->_command; + } + + /** + * Add an option to the option parser. Options allow you to define optional or required + * parameters for your console application. Options are defined by the parameters they use. + * + * ### Options + * + * - `short` - The single letter variant for this option, leave undefined for none. + * - `help` - Help text for this option. Used when generating help for the option. + * - `default` - The default value for this option. Defaults are added into the parsed params when the + * attached option is not provided or has no value. Using default and boolean together will not work. + * are added into the parsed parameters when the option is undefined. Defaults to null. + * - `boolean` - The option uses no value, its just a boolean switch. Defaults to false. + * If an option is defined as boolean, it will always be added to the parsed params. If no present + * it will be false, if present it will be true. + * - `choices` A list of valid choices for this option. If left empty all values are valid.. + * An exception will be raised when parse() encounters an invalid value. + * + * @param ConsoleInputOption|string $name The long name you want to the value to be parsed out as when options are parsed. + * Will also accept an instance of ConsoleInputOption + * @param array $options An array of parameters that define the behavior of the option + * @return self + */ + public function addOption($name, $options = []) + { + if (is_object($name) && $name instanceof ConsoleInputOption) { + $option = $name; + $name = $option->name(); + } else { + $defaults = [ + 'name' => $name, + 'short' => null, + 'help' => '', + 'default' => null, + 'boolean' => false, + 'choices' => [] + ]; + $options += $defaults; + $option = new ConsoleInputOption($options); + } + $this->_options[$name] = $option; + if ($option->short() !== null) { + $this->_shortOptions[$option->short()] = $name; + } + return $this; + } + + /** + * Static factory method for creating new OptionParsers so you can chain methods off of them. + * + * @param string $command The command name this parser is for. The command name is used for generating help. + * @param bool $defaultOptions Whether you want the verbose and quiet options set. + * @return ConsoleOptionParser + */ + public static function create($command, $defaultOptions = true) + { + return new ConsoleOptionParser($command, $defaultOptions); + } + + /** + * Build a parser from an array. Uses an array like + * + * ``` + * $spec = array( + * 'description' => 'text', + * 'epilog' => 'text', + * 'arguments' => array( + * // list of arguments compatible with addArguments. + * ), + * 'options' => array( + * // list of options compatible with addOptions + * ), + * 'subcommands' => array( + * // list of subcommands to add. + * ) + * ); + * ``` + * + * @param array $spec The spec to build the OptionParser with. + * @return ConsoleOptionParser + */ + public static function buildFromArray($spec) + { + $parser = new ConsoleOptionParser($spec['command']); + if (!empty($spec['arguments'])) { + $parser->addArguments($spec['arguments']); + } + if (!empty($spec['options'])) { + $parser->addOptions($spec['options']); + } + if (!empty($spec['subcommands'])) { + $parser->addSubcommands($spec['subcommands']); + } + if (!empty($spec['description'])) { + $parser->description($spec['description']); + } + if (!empty($spec['epilog'])) { + $parser->epilog($spec['epilog']); + } + return $parser; + } + + /** + * Add multiple arguments at once. Take an array of argument definitions. + * The keys are used as the argument names, and the values as params for the argument. + * + * @param array $args Array of arguments to add. + * @return self + * @see ConsoleOptionParser::addArgument() + */ + public function addArguments(array $args) + { + foreach ($args as $name => $params) { + $this->addArgument($name, $params); + } + return $this; + } + + /** + * Add a positional argument to the option parser. + * + * ### Params + * + * - `help` The help text to display for this argument. + * - `required` Whether this parameter is required. + * - `index` The index for the arg, if left undefined the argument will be put + * onto the end of the arguments. If you define the same index twice the first + * option will be overwritten. + * - `choices` A list of valid choices for this argument. If left empty all values are valid.. + * An exception will be raised when parse() encounters an invalid value. + * + * @param ConsoleInputArgument|string $name The name of the argument. Will also accept an instance of ConsoleInputArgument + * @param array $params Parameters for the argument, see above. + * @return self + */ + public function addArgument($name, $params = []) + { + if (is_object($name) && $name instanceof ConsoleInputArgument) { + $arg = $name; + $index = count($this->_args); + } else { + $defaults = [ + 'name' => $name, + 'help' => '', + 'index' => count($this->_args), + 'required' => false, + 'choices' => [] + ]; + $options = $params + $defaults; + $index = $options['index']; + unset($options['index']); + $arg = new ConsoleInputArgument($options); + } + $this->_args[$index] = $arg; + ksort($this->_args); + return $this; + } + + /** + * Add multiple options at once. Takes an array of option definitions. + * The keys are used as option names, and the values as params for the option. + * + * @param array $options Array of options to add. + * @return self + * @see ConsoleOptionParser::addOption() + */ + public function addOptions(array $options) + { + foreach ($options as $name => $params) { + $this->addOption($name, $params); + } + return $this; + } + + /** + * Add multiple subcommands at once. + * + * @param array $commands Array of subcommands. + * @return self + */ + public function addSubcommands(array $commands) + { + foreach ($commands as $name => $params) { + $this->addSubcommand($name, $params); + } + return $this; + } + + /** + * Append a subcommand to the subcommand list. + * Subcommands are usually methods on your Shell, but can also be used to document Tasks. + * + * ### Options + * + * - `help` - Help text for the subcommand. + * - `parser` - A ConsoleOptionParser for the subcommand. This allows you to create method + * specific option parsers. When help is generated for a subcommand, if a parser is present + * it will be used. + * + * @param ConsoleInputSubcommand|string $name Name of the subcommand. Will also accept an instance of ConsoleInputSubcommand + * @param array $options Array of params, see above. + * @return self + */ + public function addSubcommand($name, $options = []) + { + if (is_object($name) && $name instanceof ConsoleInputSubcommand) { + $command = $name; + $name = $command->name(); + } else { + $defaults = [ + 'name' => $name, + 'help' => '', + 'parser' => null + ]; + $options += $defaults; + $command = new ConsoleInputSubcommand($options); + } + $this->_subcommands[$name] = $command; + return $this; + } + + /** + * Get or set the description text for shell/task. + * + * @param string|array $text The text to set, or null if you want to read. If an array the + * text will be imploded with "\n" + * @return string|self If reading, the value of the description. If setting $this will be returned. + */ + public function description($text = null) + { + if ($text !== null) { + if (is_array($text)) { + $text = implode("\n", $text); + } + $this->_description = $text; + return $this; + } + return $this->_description; + } + + /** + * Get or set an epilog to the parser. The epilog is added to the end of + * the options and arguments listing when help is generated. + * + * @param string|array $text Text when setting or null when reading. If an array the text will be imploded with "\n" + * @return string|self If reading, the value of the epilog. If setting $this will be returned. + */ + public function epilog($text = null) + { + if ($text !== null) { + if (is_array($text)) { + $text = implode("\n", $text); + } + $this->_epilog = $text; + return $this; + } + return $this->_epilog; + } + + /** + * Remove a subcommand from the option parser. + * + * @param string $name The subcommand name to remove. + * @return self + */ + public function removeSubcommand($name) + { + unset($this->_subcommands[$name]); + return $this; + } + + /** + * Gets the arguments defined in the parser. + * + * @return array Array of argument descriptions + */ + public function arguments() + { + return $this->_args; + } + + /** + * Get the defined options in the parser. + * + * @return array + */ + public function options() + { + return $this->_options; + } + + /** + * Get the array of defined subcommands + * + * @return array + */ + public function subcommands() + { + return $this->_subcommands; + } + + /** + * Parse the argv array into a set of params and args. If $command is not null + * and $command is equal to a subcommand that has a parser, that parser will be used + * to parse the $argv + * + * @param array $argv Array of args (argv) to parse. + * @param string $command The subcommand to use. If this parameter is a subcommand, that has a parser, + * That parser will be used to parse $argv instead. + * @return array array($params, $args) + * @throws ConsoleException When an invalid parameter is encountered. + */ + public function parse($argv, $command = null) + { + if (isset($this->_subcommands[$command]) && $this->_subcommands[$command]->parser()) { + return $this->_subcommands[$command]->parser()->parse($argv); + } + $params = $args = []; + $this->_tokens = $argv; + while (($token = array_shift($this->_tokens)) !== null) { + if (substr($token, 0, 2) === '--') { + $params = $this->_parseLongOption($token, $params); + } else if (substr($token, 0, 1) === '-') { + $params = $this->_parseShortOption($token, $params); + } else { + $args = $this->_parseArg($token, $args); + } + } + foreach ($this->_args as $i => $arg) { + if ($arg->isRequired() && !isset($args[$i]) && empty($params['help'])) { + throw new ConsoleException( + __d('cake_console', 'Missing required arguments. %s is required.', $arg->name()) + ); + } + } + foreach ($this->_options as $option) { + $name = $option->name(); + $isBoolean = $option->isBoolean(); + $default = $option->defaultValue(); + + if ($default !== null && !isset($params[$name]) && !$isBoolean) { + $params[$name] = $default; + } + if ($isBoolean && !isset($params[$name])) { + $params[$name] = false; + } + } + return [$params, $args]; + } + + /** + * Parse the value for a long option out of $this->_tokens. Will handle + * options with an `=` in them. + * + * @param string $option The option to parse. + * @param array $params The params to append the parsed value into + * @return array Params with $option added in. + */ + protected function _parseLongOption($option, $params) + { + $name = substr($option, 2); + if (strpos($name, '=') !== false) { + list($name, $value) = explode('=', $name, 2); + array_unshift($this->_tokens, $value); + } + return $this->_parseOption($name, $params); + } + + /** + * Parse an option by its name index. + * + * @param string $name The name to parse. + * @param array $params The params to append the parsed value into + * @return array Params with $option added in. + * @throws ConsoleException + */ + protected function _parseOption($name, $params) + { + if (!isset($this->_options[$name])) { + throw new ConsoleException(__d('cake_console', 'Unknown option `%s`', $name)); + } + $option = $this->_options[$name]; + $isBoolean = $option->isBoolean(); + $nextValue = $this->_nextToken(); + $emptyNextValue = (empty($nextValue) && $nextValue !== '0'); + if (!$isBoolean && !$emptyNextValue && !$this->_optionExists($nextValue)) { + array_shift($this->_tokens); + $value = $nextValue; + } else if ($isBoolean) { + $value = true; + } else { + $value = $option->defaultValue(); + } + if ($option->validChoice($value)) { + $params[$name] = $value; + return $params; + } + return []; + } + + /** + * Find the next token in the argv set. + * + * @return string next token or '' + */ + protected function _nextToken() + { + return isset($this->_tokens[0]) ? $this->_tokens[0] : ''; + } + + /** + * Check to see if $name has an option (short/long) defined for it. + * + * @param string $name The name of the option. + * @return bool + */ + protected function _optionExists($name) + { + if (substr($name, 0, 2) === '--') { + return isset($this->_options[substr($name, 2)]); + } + if ($name{0} === '-' && $name{1} !== '-') { + return isset($this->_shortOptions[$name{1}]); + } + return false; + } + + /** + * Parse the value for a short option out of $this->_tokens + * If the $option is a combination of multiple shortcuts like -otf + * they will be shifted onto the token stack and parsed individually. + * + * @param string $option The option to parse. + * @param array $params The params to append the parsed value into + * @return array Params with $option added in. + * @throws ConsoleException When unknown short options are encountered. + */ + protected function _parseShortOption($option, $params) + { + $key = substr($option, 1); + if (strlen($key) > 1) { + $flags = str_split($key); + $key = $flags[0]; + for ($i = 1, $len = count($flags); $i < $len; $i++) { + array_unshift($this->_tokens, '-' . $flags[$i]); + } + } + if (!isset($this->_shortOptions[$key])) { + throw new ConsoleException(__d('cake_console', 'Unknown short option `%s`', $key)); + } + $name = $this->_shortOptions[$key]; + return $this->_parseOption($name, $params); + } + + /** + * Parse an argument, and ensure that the argument doesn't exceed the number of arguments + * and that the argument is a valid choice. + * + * @param string $argument The argument to append + * @param array $args The array of parsed args to append to. + * @return array Args + * @throws ConsoleException + */ + protected function _parseArg($argument, $args) + { + if (empty($this->_args)) { + $args[] = $argument; + return $args; + } + $next = count($args); + if (!isset($this->_args[$next])) { + throw new ConsoleException(__d('cake_console', 'Too many arguments.')); + } + + if ($this->_args[$next]->validChoice($argument)) { + $args[] = $argument; + return $args; + } + } + + /** + * Gets formatted help for this parser object. + * Generates help text based on the description, options, arguments, subcommands and epilog + * in the parser. + * + * @param string $subcommand If present and a valid subcommand that has a linked parser. + * That subcommands help will be shown instead. + * @param string $format Define the output format, can be text or xml + * @param int $width The width to format user content to. Defaults to 72 + * @return string Generated help. + */ + public function help($subcommand = null, $format = 'text', $width = 72) + { + if (isset($this->_subcommands[$subcommand]) && + $this->_subcommands[$subcommand]->parser() instanceof self + ) { + $subparser = $this->_subcommands[$subcommand]->parser(); + $subparser->command($this->command() . ' ' . $subparser->command()); + return $subparser->help(null, $format, $width); + } + $formatter = new HelpFormatter($this); + if ($format === 'text' || $format === true) { + return $formatter->text($width); + } else if ($format === 'xml') { + return $formatter->xml(); + } + } } diff --git a/lib/Cake/Console/ConsoleOutput.php b/lib/Cake/Console/ConsoleOutput.php index 87e3fd2a..e07d7421 100755 --- a/lib/Cake/Console/ConsoleOutput.php +++ b/lib/Cake/Console/ConsoleOutput.php @@ -42,7 +42,8 @@ * * @package Cake.Console */ -class ConsoleOutput { +class ConsoleOutput +{ /** * Raw output constant - no modification of output text. @@ -71,35 +72,12 @@ class ConsoleOutput { * @var string */ const LF = PHP_EOL; - - /** - * File handle for output. - * - * @var resource - */ - protected $_output; - - /** - * The number of bytes last written to the output stream - * used when overwriting the previous message. - * - * @var int - */ - protected $_lastWritten = 0; - - /** - * The current output type. Manipulated with ConsoleOutput::outputAs(); - * - * @var int - */ - protected $_outputAs = self::COLOR; - /** * text colors used in colored output. * * @var array */ - protected static $_foregroundColors = array( + protected static $_foregroundColors = [ 'black' => 30, 'red' => 31, 'green' => 32, @@ -108,14 +86,13 @@ class ConsoleOutput { 'magenta' => 35, 'cyan' => 36, 'white' => 37 - ); - + ]; /** * background colors used in colored output. * * @var array */ - protected static $_backgroundColors = array( + protected static $_backgroundColors = [ 'black' => 40, 'red' => 41, 'green' => 42, @@ -124,39 +101,56 @@ class ConsoleOutput { 'magenta' => 45, 'cyan' => 46, 'white' => 47 - ); - + ]; /** * formatting options for colored output * * @var string */ - protected static $_options = array( + protected static $_options = [ 'bold' => 1, 'underline' => 4, 'blink' => 5, 'reverse' => 7, - ); - + ]; /** * Styles that are available as tags in console output. * You can modify these styles with ConsoleOutput::styles() * * @var array */ - protected static $_styles = array( - 'emergency' => array('text' => 'red', 'underline' => true), - 'alert' => array('text' => 'red', 'underline' => true), - 'critical' => array('text' => 'red', 'underline' => true), - 'error' => array('text' => 'red', 'underline' => true), - 'warning' => array('text' => 'yellow'), - 'info' => array('text' => 'cyan'), - 'debug' => array('text' => 'yellow'), - 'success' => array('text' => 'green'), - 'comment' => array('text' => 'blue'), - 'question' => array('text' => 'magenta'), - 'notice' => array('text' => 'cyan') - ); + protected static $_styles = [ + 'emergency' => ['text' => 'red', 'underline' => true], + 'alert' => ['text' => 'red', 'underline' => true], + 'critical' => ['text' => 'red', 'underline' => true], + 'error' => ['text' => 'red', 'underline' => true], + 'warning' => ['text' => 'yellow'], + 'info' => ['text' => 'cyan'], + 'debug' => ['text' => 'yellow'], + 'success' => ['text' => 'green'], + 'comment' => ['text' => 'blue'], + 'question' => ['text' => 'magenta'], + 'notice' => ['text' => 'cyan'] + ]; + /** + * File handle for output. + * + * @var resource + */ + protected $_output; + /** + * The number of bytes last written to the output stream + * used when overwriting the previous message. + * + * @var int + */ + protected $_lastWritten = 0; + /** + * The current output type. Manipulated with ConsoleOutput::outputAs(); + * + * @var int + */ + protected $_outputAs = self::COLOR; /** * Construct the output object. @@ -166,7 +160,8 @@ class ConsoleOutput { * * @param string $stream The identifier of the stream to write output to. */ - public function __construct($stream = 'php://stdout') { + public function __construct($stream = 'php://stdout') + { $this->_output = fopen($stream, 'w'); if ((DS === '\\' && !(bool)env('ANSICON') && env('ConEmuANSI') !== 'ON') || @@ -177,21 +172,6 @@ public function __construct($stream = 'php://stdout') { } } - /** - * Outputs a single or multiple messages to stdout. If no parameters - * are passed, outputs just a newline. - * - * @param string|array $message A string or an array of strings to output - * @param int $newlines Number of newlines to append - * @return int Returns the number of bytes returned from writing to stdout. - */ - public function write($message, $newlines = 1) { - if (is_array($message)) { - $message = implode(static::LF, $message); - } - return $this->_write($this->styleText($message . str_repeat(static::LF, $newlines))); - } - /** * Overwrite some already output text. * @@ -206,7 +186,8 @@ public function write($message, $newlines = 1) { * length of the last message output. * @return void */ - public function overwrite($message, $newlines = 1, $size = null) { + public function overwrite($message, $newlines = 1, $size = null) + { $size = $size ?: $this->_lastWritten; // Output backspaces. $this->write(str_repeat("\x08", $size), 0); @@ -221,13 +202,42 @@ public function overwrite($message, $newlines = 1, $size = null) { } } + /** + * Outputs a single or multiple messages to stdout. If no parameters + * are passed, outputs just a newline. + * + * @param string|array $message A string or an array of strings to output + * @param int $newlines Number of newlines to append + * @return int Returns the number of bytes returned from writing to stdout. + */ + public function write($message, $newlines = 1) + { + if (is_array($message)) { + $message = implode(static::LF, $message); + } + return $this->_write($this->styleText($message . str_repeat(static::LF, $newlines))); + } + + /** + * Writes a message to the output stream. + * + * @param string $message Message to write. + * @return bool success + */ + protected function _write($message) + { + $this->_lastWritten = fwrite($this->_output, $message); + return $this->_lastWritten; + } + /** * Apply styling to text. * * @param string $text Text with styling tags. * @return string String with color codes added. */ - public function styleText($text) { + public function styleText($text) + { if ($this->_outputAs == static::RAW) { return $text; } @@ -236,23 +246,48 @@ public function styleText($text) { return preg_replace('##', '', $text); } return preg_replace_callback( - '/<(?P[a-z0-9-_]+)>(?P.*?)<\/(\1)>/ims', array($this, '_replaceTags'), $text + '/<(?P[a-z0-9-_]+)>(?P.*?)<\/(\1)>/ims', [$this, '_replaceTags'], $text ); } + /** + * Get/Set the output type to use. The output type how formatting tags are treated. + * + * @param int $type The output type to use. Should be one of the class constants. + * @return mixed Either null or the value if getting. + */ + public function outputAs($type = null) + { + if ($type === null) { + return $this->_outputAs; + } + $this->_outputAs = $type; + } + + /** + * Clean up and close handles + */ + public function __destruct() + { + if (is_resource($this->_output)) { + fclose($this->_output); + } + } + /** * Replace tags with color codes. * * @param array $matches An array of matches to replace. * @return string */ - protected function _replaceTags($matches) { + protected function _replaceTags($matches) + { $style = $this->styles($matches['tag']); if (empty($style)) { return '<' . $matches['tag'] . '>' . $matches['text'] . ''; } - $styleInfo = array(); + $styleInfo = []; if (!empty($style['text']) && isset(static::$_foregroundColors[$style['text']])) { $styleInfo[] = static::$_foregroundColors[$style['text']]; } @@ -268,17 +303,6 @@ protected function _replaceTags($matches) { return "\033[" . implode(';', $styleInfo) . 'm' . $matches['text'] . "\033[0m"; } - /** - * Writes a message to the output stream. - * - * @param string $message Message to write. - * @return bool success - */ - protected function _write($message) { - $this->_lastWritten = fwrite($this->_output, $message); - return $this->_lastWritten; - } - /** * Get the current styles offered, or append new ones in. * @@ -304,7 +328,8 @@ protected function _write($message) { * @return mixed If you are getting styles, the style or null will be returned. If you are creating/modifying * styles true will be returned. */ - public function styles($style = null, $definition = null) { + public function styles($style = null, $definition = null) + { if ($style === null && $definition === null) { return static::$_styles; } @@ -319,26 +344,4 @@ public function styles($style = null, $definition = null) { return true; } - /** - * Get/Set the output type to use. The output type how formatting tags are treated. - * - * @param int $type The output type to use. Should be one of the class constants. - * @return mixed Either null or the value if getting. - */ - public function outputAs($type = null) { - if ($type === null) { - return $this->_outputAs; - } - $this->_outputAs = $type; - } - - /** - * Clean up and close handles - */ - public function __destruct() { - if (is_resource($this->_output)) { - fclose($this->_output); - } - } - } \ No newline at end of file diff --git a/lib/Cake/Console/HelpFormatter.php b/lib/Cake/Console/HelpFormatter.php index 0c4259aa..1bd29e14 100755 --- a/lib/Cake/Console/HelpFormatter.php +++ b/lib/Cake/Console/HelpFormatter.php @@ -28,174 +28,180 @@ * @package Cake.Console * @since CakePHP(tm) v 2.0 */ -class HelpFormatter { +class HelpFormatter +{ -/** - * The maximum number of arguments shown when generating usage. - * - * @var int - */ - protected $_maxArgs = 6; + /** + * The maximum number of arguments shown when generating usage. + * + * @var int + */ + protected $_maxArgs = 6; -/** - * The maximum number of options shown when generating usage. - * - * @var int - */ - protected $_maxOptions = 6; + /** + * The maximum number of options shown when generating usage. + * + * @var int + */ + protected $_maxOptions = 6; -/** - * Build the help formatter for an OptionParser - * - * @param ConsoleOptionParser $parser The option parser help is being generated for. - */ - public function __construct(ConsoleOptionParser $parser) { - $this->_parser = $parser; - } + /** + * Build the help formatter for an OptionParser + * + * @param ConsoleOptionParser $parser The option parser help is being generated for. + */ + public function __construct(ConsoleOptionParser $parser) + { + $this->_parser = $parser; + } -/** - * Get the help as formatted text suitable for output on the command line. - * - * @param int $width The width of the help output. - * @return string - */ - public function text($width = 72) { - $parser = $this->_parser; - $out = array(); - $description = $parser->description(); - if (!empty($description)) { - $out[] = CakeText::wrap($description, $width); - $out[] = ''; - } - $out[] = __d('cake_console', 'Usage:'); - $out[] = $this->_generateUsage(); - $out[] = ''; - $subcommands = $parser->subcommands(); - if (!empty($subcommands)) { - $out[] = __d('cake_console', 'Subcommands:'); - $out[] = ''; - $max = $this->_getMaxLength($subcommands) + 2; - foreach ($subcommands as $command) { - $out[] = CakeText::wrap($command->help($max), array( - 'width' => $width, - 'indent' => str_repeat(' ', $max), - 'indentAt' => 1 - )); - } - $out[] = ''; - $out[] = __d('cake_console', 'To see help on a subcommand use `cake %s [subcommand] --help`', $parser->command()); - $out[] = ''; - } + /** + * Get the help as formatted text suitable for output on the command line. + * + * @param int $width The width of the help output. + * @return string + */ + public function text($width = 72) + { + $parser = $this->_parser; + $out = []; + $description = $parser->description(); + if (!empty($description)) { + $out[] = CakeText::wrap($description, $width); + $out[] = ''; + } + $out[] = __d('cake_console', 'Usage:'); + $out[] = $this->_generateUsage(); + $out[] = ''; + $subcommands = $parser->subcommands(); + if (!empty($subcommands)) { + $out[] = __d('cake_console', 'Subcommands:'); + $out[] = ''; + $max = $this->_getMaxLength($subcommands) + 2; + foreach ($subcommands as $command) { + $out[] = CakeText::wrap($command->help($max), [ + 'width' => $width, + 'indent' => str_repeat(' ', $max), + 'indentAt' => 1 + ]); + } + $out[] = ''; + $out[] = __d('cake_console', 'To see help on a subcommand use `cake %s [subcommand] --help`', $parser->command()); + $out[] = ''; + } - $options = $parser->options(); - if (!empty($options)) { - $max = $this->_getMaxLength($options) + 8; - $out[] = __d('cake_console', 'Options:'); - $out[] = ''; - foreach ($options as $option) { - $out[] = CakeText::wrap($option->help($max), array( - 'width' => $width, - 'indent' => str_repeat(' ', $max), - 'indentAt' => 1 - )); - } - $out[] = ''; - } + $options = $parser->options(); + if (!empty($options)) { + $max = $this->_getMaxLength($options) + 8; + $out[] = __d('cake_console', 'Options:'); + $out[] = ''; + foreach ($options as $option) { + $out[] = CakeText::wrap($option->help($max), [ + 'width' => $width, + 'indent' => str_repeat(' ', $max), + 'indentAt' => 1 + ]); + } + $out[] = ''; + } - $arguments = $parser->arguments(); - if (!empty($arguments)) { - $max = $this->_getMaxLength($arguments) + 2; - $out[] = __d('cake_console', 'Arguments:'); - $out[] = ''; - foreach ($arguments as $argument) { - $out[] = CakeText::wrap($argument->help($max), array( - 'width' => $width, - 'indent' => str_repeat(' ', $max), - 'indentAt' => 1 - )); - } - $out[] = ''; - } - $epilog = $parser->epilog(); - if (!empty($epilog)) { - $out[] = CakeText::wrap($epilog, $width); - $out[] = ''; - } - return implode("\n", $out); - } + $arguments = $parser->arguments(); + if (!empty($arguments)) { + $max = $this->_getMaxLength($arguments) + 2; + $out[] = __d('cake_console', 'Arguments:'); + $out[] = ''; + foreach ($arguments as $argument) { + $out[] = CakeText::wrap($argument->help($max), [ + 'width' => $width, + 'indent' => str_repeat(' ', $max), + 'indentAt' => 1 + ]); + } + $out[] = ''; + } + $epilog = $parser->epilog(); + if (!empty($epilog)) { + $out[] = CakeText::wrap($epilog, $width); + $out[] = ''; + } + return implode("\n", $out); + } -/** - * Generate the usage for a shell based on its arguments and options. - * Usage strings favor short options over the long ones. and optional args will - * be indicated with [] - * - * @return string - */ - protected function _generateUsage() { - $usage = array('cake ' . $this->_parser->command()); - $subcommands = $this->_parser->subcommands(); - if (!empty($subcommands)) { - $usage[] = '[subcommand]'; - } - $options = array(); - foreach ($this->_parser->options() as $option) { - $options[] = $option->usage(); - } - if (count($options) > $this->_maxOptions) { - $options = array('[options]'); - } - $usage = array_merge($usage, $options); - $args = array(); - foreach ($this->_parser->arguments() as $argument) { - $args[] = $argument->usage(); - } - if (count($args) > $this->_maxArgs) { - $args = array('[arguments]'); - } - $usage = array_merge($usage, $args); - return implode(' ', $usage); - } + /** + * Generate the usage for a shell based on its arguments and options. + * Usage strings favor short options over the long ones. and optional args will + * be indicated with [] + * + * @return string + */ + protected function _generateUsage() + { + $usage = ['cake ' . $this->_parser->command()]; + $subcommands = $this->_parser->subcommands(); + if (!empty($subcommands)) { + $usage[] = '[subcommand]'; + } + $options = []; + foreach ($this->_parser->options() as $option) { + $options[] = $option->usage(); + } + if (count($options) > $this->_maxOptions) { + $options = ['[options]']; + } + $usage = array_merge($usage, $options); + $args = []; + foreach ($this->_parser->arguments() as $argument) { + $args[] = $argument->usage(); + } + if (count($args) > $this->_maxArgs) { + $args = ['[arguments]']; + } + $usage = array_merge($usage, $args); + return implode(' ', $usage); + } -/** - * Iterate over a collection and find the longest named thing. - * - * @param array $collection The collection to find a max length of. - * @return int - */ - protected function _getMaxLength($collection) { - $max = 0; - foreach ($collection as $item) { - $max = (strlen($item->name()) > $max) ? strlen($item->name()) : $max; - } - return $max; - } + /** + * Iterate over a collection and find the longest named thing. + * + * @param array $collection The collection to find a max length of. + * @return int + */ + protected function _getMaxLength($collection) + { + $max = 0; + foreach ($collection as $item) { + $max = (strlen($item->name()) > $max) ? strlen($item->name()) : $max; + } + return $max; + } -/** - * Get the help as an xml string. - * - * @param bool $string Return the SimpleXml object or a string. Defaults to true. - * @return string|SimpleXmlElement See $string - */ - public function xml($string = true) { - $parser = $this->_parser; - $xml = new SimpleXmlElement(''); - $xml->addChild('command', $parser->command()); - $xml->addChild('description', $parser->description()); + /** + * Get the help as an xml string. + * + * @param bool $string Return the SimpleXml object or a string. Defaults to true. + * @return string|SimpleXmlElement See $string + */ + public function xml($string = true) + { + $parser = $this->_parser; + $xml = new SimpleXmlElement(''); + $xml->addChild('command', $parser->command()); + $xml->addChild('description', $parser->description()); - $subcommands = $xml->addChild('subcommands'); - foreach ($parser->subcommands() as $command) { - $command->xml($subcommands); - } - $options = $xml->addChild('options'); - foreach ($parser->options() as $option) { - $option->xml($options); - } - $arguments = $xml->addChild('arguments'); - foreach ($parser->arguments() as $argument) { - $argument->xml($arguments); - } - $xml->addChild('epilog', $parser->epilog()); - return $string ? $xml->asXml() : $xml; - } + $subcommands = $xml->addChild('subcommands'); + foreach ($parser->subcommands() as $command) { + $command->xml($subcommands); + } + $options = $xml->addChild('options'); + foreach ($parser->options() as $option) { + $option->xml($options); + } + $arguments = $xml->addChild('arguments'); + foreach ($parser->arguments() as $argument) { + $argument->xml($arguments); + } + $xml->addChild('epilog', $parser->epilog()); + return $string ? $xml->asXml() : $xml; + } } diff --git a/lib/Cake/Console/Helper/BaseShellHelper.php b/lib/Cake/Console/Helper/BaseShellHelper.php index 96f12c1b..6804f996 100644 --- a/lib/Cake/Console/Helper/BaseShellHelper.php +++ b/lib/Cake/Console/Helper/BaseShellHelper.php @@ -13,70 +13,73 @@ * @license https://opensource.org/licenses/mit-license.php MIT License */ -abstract class BaseShellHelper { +abstract class BaseShellHelper +{ -/** - * Default config for this helper. - * - * @var array - */ - protected $_defaultConfig = array(); + /** + * Default config for this helper. + * + * @var array + */ + protected $_defaultConfig = []; -/** - * ConsoleOutput instance. - * - * @var ConsoleOutput - */ - protected $_consoleOutput; + /** + * ConsoleOutput instance. + * + * @var ConsoleOutput + */ + protected $_consoleOutput; -/** - * Runtime config - * - * @var array - */ - protected $_config = array(); + /** + * Runtime config + * + * @var array + */ + protected $_config = []; -/** - * Whether the config property has already been configured with defaults - * - * @var bool - */ - protected $_configInitialized = false; + /** + * Whether the config property has already been configured with defaults + * + * @var bool + */ + protected $_configInitialized = false; -/** - * Constructor. - * - * @param ConsoleOutput $consoleOutput The ConsoleOutput instance to use. - * @param array $config The settings for this helper. - */ - public function __construct(ConsoleOutput $consoleOutput, array $config = array()) { - $this->_consoleOutput = $consoleOutput; - $this->config($config); - } + /** + * Constructor. + * + * @param ConsoleOutput $consoleOutput The ConsoleOutput instance to use. + * @param array $config The settings for this helper. + */ + public function __construct(ConsoleOutput $consoleOutput, array $config = []) + { + $this->_consoleOutput = $consoleOutput; + $this->config($config); + } -/** - * Initialize config & store config values - * - * @param null $config Config values to set - * @return array|void - */ - public function config($config = null) { - if ($config === null) { - return $this->_config; - } - if (!$this->_configInitialized) { - $this->_config = array_merge($this->_defaultConfig, $config); - $this->_configInitialized = true; - } else { - $this->_config = array_merge($this->_config, $config); - } - } + /** + * Initialize config & store config values + * + * @param null $config Config values to set + * @return array|void + */ + public function config($config = null) + { + if ($config === null) { + return $this->_config; + } + if (!$this->_configInitialized) { + $this->_config = array_merge($this->_defaultConfig, $config); + $this->_configInitialized = true; + } else { + $this->_config = array_merge($this->_config, $config); + } + } -/** - * This method should output content using `$this->_consoleOutput`. - * - * @param array $args The arguments for the helper. - * @return void - */ - abstract public function output($args); + /** + * This method should output content using `$this->_consoleOutput`. + * + * @param array $args The arguments for the helper. + * @return void + */ + abstract public function output($args); } \ No newline at end of file diff --git a/lib/Cake/Console/Helper/ProgressShellHelper.php b/lib/Cake/Console/Helper/ProgressShellHelper.php index c5c615f7..92beb0dd 100644 --- a/lib/Cake/Console/Helper/ProgressShellHelper.php +++ b/lib/Cake/Console/Helper/ProgressShellHelper.php @@ -17,106 +17,111 @@ /** * Create a progress bar using a supplied callback. */ -class ProgressShellHelper extends BaseShellHelper { +class ProgressShellHelper extends BaseShellHelper +{ -/** - * The current progress. - * - * @var int - */ - protected $_progress = 0; + /** + * The current progress. + * + * @var int + */ + protected $_progress = 0; -/** - * The total number of 'items' to progress through. - * - * @var int - */ - protected $_total = 0; + /** + * The total number of 'items' to progress through. + * + * @var int + */ + protected $_total = 0; -/** - * The width of the bar. - * - * @var int - */ - protected $_width = 0; + /** + * The width of the bar. + * + * @var int + */ + protected $_width = 0; -/** - * Output a progress bar. - * - * Takes a number of options to customize the behavior: - * - * - `total` The total number of items in the progress bar. Defaults - * to 100. - * - `width` The width of the progress bar. Defaults to 80. - * - `callback` The callback that will be called in a loop to advance the progress bar. - * - * @param array $args The arguments/options to use when outputing the progress bar. - * @return void - * @throws RuntimeException - */ - public function output($args) { - $args += array('callback' => null); - if (isset($args[0])) { - $args['callback'] = $args[0]; - } - if (!$args['callback'] || !is_callable($args['callback'])) { - throw new RuntimeException('Callback option must be a callable.'); - } - $this->init($args); - $callback = $args['callback']; - while ($this->_progress < $this->_total) { - $callback($this); - $this->draw(); - } - $this->_consoleOutput->write(''); - } + /** + * Output a progress bar. + * + * Takes a number of options to customize the behavior: + * + * - `total` The total number of items in the progress bar. Defaults + * to 100. + * - `width` The width of the progress bar. Defaults to 80. + * - `callback` The callback that will be called in a loop to advance the progress bar. + * + * @param array $args The arguments/options to use when outputing the progress bar. + * @return void + * @throws RuntimeException + */ + public function output($args) + { + $args += ['callback' => null]; + if (isset($args[0])) { + $args['callback'] = $args[0]; + } + if (!$args['callback'] || !is_callable($args['callback'])) { + throw new RuntimeException('Callback option must be a callable.'); + } + $this->init($args); + $callback = $args['callback']; + while ($this->_progress < $this->_total) { + $callback($this); + $this->draw(); + } + $this->_consoleOutput->write(''); + } -/** - * Initialize the progress bar for use. - * - * - `total` The total number of items in the progress bar. Defaults - * to 100. - * - `width` The width of the progress bar. Defaults to 80. - * - * @param array $args The initialization data. - * @return void - */ - public function init(array $args = array()) { - $args += array('total' => 100, 'width' => 80); - $this->_progress = 0; - $this->_width = $args['width']; - $this->_total = $args['total']; - } + /** + * Initialize the progress bar for use. + * + * - `total` The total number of items in the progress bar. Defaults + * to 100. + * - `width` The width of the progress bar. Defaults to 80. + * + * @param array $args The initialization data. + * @return void + */ + public function init(array $args = []) + { + $args += ['total' => 100, 'width' => 80]; + $this->_progress = 0; + $this->_width = $args['width']; + $this->_total = $args['total']; + } -/** - * Increment the progress bar. - * - * @param int $num The amount of progress to advance by. - * @return void - */ - public function increment($num = 1) { - $this->_progress = min(max(0, $this->_progress + $num), $this->_total); - } + /** + * Render the progress bar based on the current state. + * + * @return void + */ + public function draw() + { + $numberLen = strlen(' 100%'); + $complete = round($this->_progress / $this->_total, 2); + $barLen = ($this->_width - $numberLen) * ($this->_progress / $this->_total); + $bar = ''; + if ($barLen > 1) { + $bar = str_repeat('=', $barLen - 1) . '>'; + } + $pad = ceil($this->_width - $numberLen - $barLen); + if ($pad > 0) { + $bar .= str_repeat(' ', $pad); + } + $percent = ($complete * 100) . '%'; + $bar .= str_pad($percent, $numberLen, ' ', STR_PAD_LEFT); + $this->_consoleOutput->overwrite($bar, 0); + } -/** - * Render the progress bar based on the current state. - * - * @return void - */ - public function draw() { - $numberLen = strlen(' 100%'); - $complete = round($this->_progress / $this->_total, 2); - $barLen = ($this->_width - $numberLen) * ($this->_progress / $this->_total); - $bar = ''; - if ($barLen > 1) { - $bar = str_repeat('=', $barLen - 1) . '>'; - } - $pad = ceil($this->_width - $numberLen - $barLen); - if ($pad > 0) { - $bar .= str_repeat(' ', $pad); - } - $percent = ($complete * 100) . '%'; - $bar .= str_pad($percent, $numberLen, ' ', STR_PAD_LEFT); - $this->_consoleOutput->overwrite($bar, 0); - } + /** + * Increment the progress bar. + * + * @param int $num The amount of progress to advance by. + * @return void + */ + public function increment($num = 1) + { + $this->_progress = min(max(0, $this->_progress + $num), $this->_total); + } } \ No newline at end of file diff --git a/lib/Cake/Console/Helper/TableShellHelper.php b/lib/Cake/Console/Helper/TableShellHelper.php index bfa52c07..f680da3a 100644 --- a/lib/Cake/Console/Helper/TableShellHelper.php +++ b/lib/Cake/Console/Helper/TableShellHelper.php @@ -18,107 +18,113 @@ * Create a visually pleasing ASCII art table * from 2 dimensional array data. */ -class TableShellHelper extends BaseShellHelper { +class TableShellHelper extends BaseShellHelper +{ -/** - * Default config for this helper. - * - * @var array - */ - protected $_defaultConfig = array( - 'headers' => true, - 'rowSeparator' => false, - 'headerStyle' => 'info', - ); + /** + * Default config for this helper. + * + * @var array + */ + protected $_defaultConfig = [ + 'headers' => true, + 'rowSeparator' => false, + 'headerStyle' => 'info', + ]; -/** - * Calculate the column widths - * - * @param array $rows The rows on which the columns width will be calculated on. - * @return array - */ - protected function _calculateWidths($rows) { - $widths = array(); - foreach ($rows as $line) { - for ($i = 0, $len = count($line); $i < $len; $i++) { - $columnLength = mb_strlen($line[$i]); - if ($columnLength > (isset($widths[$i]) ? $widths[$i] : 0)) { - $widths[$i] = $columnLength; - } - } - } - return $widths; - } + /** + * Output a table. + * + * @param array $rows The data to render out. + * @return void + */ + public function output($rows) + { + $config = $this->config(); + $widths = $this->_calculateWidths($rows); + $this->_rowSeparator($widths); + if ($config['headers'] === true) { + $this->_render(array_shift($rows), $widths, ['style' => $config['headerStyle']]); + $this->_rowSeparator($widths); + } + foreach ($rows as $line) { + $this->_render($line, $widths); + if ($config['rowSeparator'] === true) { + $this->_rowSeparator($widths); + } + } + if ($config['rowSeparator'] !== true) { + $this->_rowSeparator($widths); + } + } -/** - * Output a row separator. - * - * @param array $widths The widths of each column to output. - * @return void - */ - protected function _rowSeparator($widths) { - $out = ''; - foreach ($widths as $column) { - $out .= '+' . str_repeat('-', $column + 2); - } - $out .= '+'; - $this->_consoleOutput->write($out); - } + /** + * Calculate the column widths + * + * @param array $rows The rows on which the columns width will be calculated on. + * @return array + */ + protected function _calculateWidths($rows) + { + $widths = []; + foreach ($rows as $line) { + for ($i = 0, $len = count($line); $i < $len; $i++) { + $columnLength = mb_strlen($line[$i]); + if ($columnLength > (isset($widths[$i]) ? $widths[$i] : 0)) { + $widths[$i] = $columnLength; + } + } + } + return $widths; + } -/** - * Output a row. - * - * @param array $row The row to output. - * @param array $widths The widths of each column to output. - * @param array $options Options to be passed. - * @return void - */ - protected function _render($row, $widths, $options = array()) { - $out = ''; - foreach ($row as $i => $column) { - $pad = $widths[$i] - mb_strlen($column); - if (!empty($options['style'])) { - $column = $this->_addStyle($column, $options['style']); - } - $out .= '| ' . $column . str_repeat(' ', $pad) . ' '; - } - $out .= '|'; - $this->_consoleOutput->write($out); - } + /** + * Output a row separator. + * + * @param array $widths The widths of each column to output. + * @return void + */ + protected function _rowSeparator($widths) + { + $out = ''; + foreach ($widths as $column) { + $out .= '+' . str_repeat('-', $column + 2); + } + $out .= '+'; + $this->_consoleOutput->write($out); + } -/** - * Output a table. - * - * @param array $rows The data to render out. - * @return void - */ - public function output($rows) { - $config = $this->config(); - $widths = $this->_calculateWidths($rows); - $this->_rowSeparator($widths); - if ($config['headers'] === true) { - $this->_render(array_shift($rows), $widths, array('style' => $config['headerStyle'])); - $this->_rowSeparator($widths); - } - foreach ($rows as $line) { - $this->_render($line, $widths); - if ($config['rowSeparator'] === true) { - $this->_rowSeparator($widths); - } - } - if ($config['rowSeparator'] !== true) { - $this->_rowSeparator($widths); - } - } + /** + * Output a row. + * + * @param array $row The row to output. + * @param array $widths The widths of each column to output. + * @param array $options Options to be passed. + * @return void + */ + protected function _render($row, $widths, $options = []) + { + $out = ''; + foreach ($row as $i => $column) { + $pad = $widths[$i] - mb_strlen($column); + if (!empty($options['style'])) { + $column = $this->_addStyle($column, $options['style']); + } + $out .= '| ' . $column . str_repeat(' ', $pad) . ' '; + } + $out .= '|'; + $this->_consoleOutput->write($out); + } -/** - * Add style tags - * - * @param string $text The text to be surrounded - * @param string $style The style to be applied - * @return string - */ - protected function _addStyle($text, $style) { - return '<' . $style . '>' . $text . ''; - } + /** + * Add style tags + * + * @param string $text The text to be surrounded + * @param string $style The style to be applied + * @return string + */ + protected function _addStyle($text, $style) + { + return '<' . $style . '>' . $text . ''; + } } \ No newline at end of file diff --git a/lib/Cake/Console/Shell.php b/lib/Cake/Console/Shell.php index c8033e69..a3912966 100755 --- a/lib/Cake/Console/Shell.php +++ b/lib/Cake/Console/Shell.php @@ -28,993 +28,1033 @@ * * @package Cake.Console */ -class Shell extends CakeObject { - -/** - * Default error code - * - * @var int - */ - const CODE_ERROR = 1; - -/** - * Output constant making verbose shells. - * - * @var int - */ - const VERBOSE = 2; - -/** - * Output constant for making normal shells. - * - * @var int - */ - const NORMAL = 1; - -/** - * Output constants for making quiet shells. - * - * @var int - */ - const QUIET = 0; - -/** - * An instance of ConsoleOptionParser that has been configured for this class. - * - * @var ConsoleOptionParser - */ - public $OptionParser; - -/** - * If true, the script will ask for permission to perform actions. - * - * @var bool - */ - public $interactive = true; - -/** - * Contains command switches parsed from the command line. - * - * @var array - */ - public $params = array(); - -/** - * The command (method/task) that is being run. - * - * @var string - */ - public $command; - -/** - * Contains arguments parsed from the command line. - * - * @var array - */ - public $args = array(); - -/** - * The name of the shell in camelized. - * - * @var string - */ - public $name = null; - -/** - * The name of the plugin the shell belongs to. - * Is automatically set by ShellDispatcher when a shell is constructed. - * - * @var string - */ - public $plugin = null; - -/** - * Contains tasks to load and instantiate - * - * @var array - * @link https://book.cakephp.org/2.0/en/console-and-shells.html#Shell::$tasks - */ - public $tasks = array(); - -/** - * Contains the loaded tasks - * - * @var array - */ - public $taskNames = array(); - -/** - * Contains models to load and instantiate - * - * @var array - * @link https://book.cakephp.org/2.0/en/console-and-shells.html#Shell::$uses - */ - public $uses = array(); - -/** - * This shell's primary model class name, the first model in the $uses property - * - * @var string - */ - public $modelClass = null; - -/** - * Task Collection for the command, used to create Tasks. - * - * @var TaskCollection - */ - public $Tasks; - -/** - * Normalized map of tasks. - * - * @var string - */ - protected $_taskMap = array(); - -/** - * stdout object. - * - * @var ConsoleOutput - */ - public $stdout; - -/** - * stderr object. - * - * @var ConsoleOutput - */ - public $stderr; - -/** - * stdin object - * - * @var ConsoleInput - */ - public $stdin; - -/** - * The number of bytes last written to the output stream - * used when overwriting the previous message. - * - * @var int - */ - protected $_lastWritten = 0; - -/** - * Contains helpers which have been previously instantiated - * - * @var array - */ - protected $_helpers = array(); - -/** - * Constructs this Shell instance. - * - * @param ConsoleOutput $stdout A ConsoleOutput object for stdout. - * @param ConsoleOutput $stderr A ConsoleOutput object for stderr. - * @param ConsoleInput $stdin A ConsoleInput object for stdin. - * @link https://book.cakephp.org/2.0/en/console-and-shells.html#Shell - */ - public function __construct($stdout = null, $stderr = null, $stdin = null) { - if (!$this->name) { - $this->name = Inflector::camelize(str_replace(array('Shell', 'Task'), '', get_class($this))); - } - $this->Tasks = new TaskCollection($this); - - $this->stdout = $stdout ? $stdout : new ConsoleOutput('php://stdout'); - $this->stderr = $stderr ? $stderr : new ConsoleOutput('php://stderr'); - $this->stdin = $stdin ? $stdin : new ConsoleInput('php://stdin'); - - $this->_useLogger(); - $parent = get_parent_class($this); - if ($this->tasks !== null && $this->tasks !== false) { - $this->_mergeVars(array('tasks'), $parent, true); - } - if (!empty($this->uses)) { - $this->_mergeVars(array('uses'), $parent, false); - } - } - -/** - * Initializes the Shell - * acts as constructor for subclasses - * allows configuration of tasks prior to shell execution - * - * @return void - * @link https://book.cakephp.org/2.0/en/console-and-shells.html#Shell::initialize - */ - public function initialize() { - $this->_loadModels(); - $this->loadTasks(); - } - -/** - * Starts up the Shell and displays the welcome message. - * Allows for checking and configuring prior to command or main execution - * - * Override this method if you want to remove the welcome information, - * or otherwise modify the pre-command flow. - * - * @return void - * @link https://book.cakephp.org/2.0/en/console-and-shells.html#Shell::startup - */ - public function startup() { - $this->_welcome(); - } - -/** - * Displays a header for the shell - * - * @return void - */ - protected function _welcome() { - $this->out(); - $this->out(__d('cake_console', 'Welcome to CakePHP %s Console', 'v' . Configure::version())); - $this->hr(); - $this->out(__d('cake_console', 'App : %s', APP_DIR)); - $this->out(__d('cake_console', 'Path: %s', APP)); - $this->hr(); - } - -/** - * If $uses is an array load each of the models in the array - * - * @return bool - */ - protected function _loadModels() { - if (is_array($this->uses)) { - list(, $this->modelClass) = pluginSplit(current($this->uses)); - foreach ($this->uses as $modelClass) { - $this->loadModel($modelClass); - } - } - return true; - } - -/** - * Lazy loads models using the loadModel() method if declared in $uses - * - * @param string $name The name of the model to look for. - * @return void - */ - public function __isset($name) { - if (is_array($this->uses)) { - foreach ($this->uses as $modelClass) { - list(, $class) = pluginSplit($modelClass); - if ($name === $class) { - return $this->loadModel($modelClass); - } - } - } - } - -/** - * Loads and instantiates models required by this shell. - * - * @param string $modelClass Name of model class to load - * @param mixed $id Initial ID the instanced model class should have - * @return mixed true when single model found and instance created, error returned if model not found. - * @throws MissingModelException if the model class cannot be found. - */ - public function loadModel($modelClass = null, $id = null) { - if ($modelClass === null) { - $modelClass = $this->modelClass; - } - - $this->uses = ($this->uses) ? (array)$this->uses : array(); - if (!in_array($modelClass, $this->uses)) { - $this->uses[] = $modelClass; - } - - list($plugin, $modelClass) = pluginSplit($modelClass, true); - if (!isset($this->modelClass)) { - $this->modelClass = $modelClass; - } - - $this->{$modelClass} = ClassRegistry::init(array( - 'class' => $plugin . $modelClass, 'alias' => $modelClass, 'id' => $id - )); - if (!$this->{$modelClass}) { - throw new MissingModelException($modelClass); - } - return true; - } - -/** - * Loads tasks defined in public $tasks - * - * @return bool - */ - public function loadTasks() { - if ($this->tasks === true || empty($this->tasks) || empty($this->Tasks)) { - return true; - } - $this->_taskMap = TaskCollection::normalizeObjectArray((array)$this->tasks); - $this->taskNames = array_merge($this->taskNames, array_keys($this->_taskMap)); - return true; - } - -/** - * Check to see if this shell has a task with the provided name. - * - * @param string $task The task name to check. - * @return bool Success - * @link https://book.cakephp.org/2.0/en/console-and-shells.html#Shell::hasTask - */ - public function hasTask($task) { - return isset($this->_taskMap[Inflector::camelize($task)]); - } - -/** - * Check to see if this shell has a callable method by the given name. - * - * @param string $name The method name to check. - * @return bool - * @link https://book.cakephp.org/2.0/en/console-and-shells.html#Shell::hasMethod - */ - public function hasMethod($name) { - try { - $method = new ReflectionMethod($this, $name); - if (!$method->isPublic() || substr($name, 0, 1) === '_') { - return false; - } - if ($method->getDeclaringClass()->name === 'Shell') { - return false; - } - return true; - } catch (ReflectionException $e) { - return false; - } - } - -/** - * Dispatch a command to another Shell. Similar to CakeObject::requestAction() - * but intended for running shells from other shells. - * - * ### Usage: - * - * With a string command: - * - * `return $this->dispatchShell('schema create DbAcl');` - * - * Avoid using this form if you have string arguments, with spaces in them. - * The dispatched will be invoked incorrectly. Only use this form for simple - * command dispatching. - * - * With an array command: - * - * `return $this->dispatchShell('schema', 'create', 'i18n', '--dry');` - * - * @return mixed The return of the other shell. - * @link https://book.cakephp.org/2.0/en/console-and-shells.html#Shell::dispatchShell - */ - public function dispatchShell() { - $args = func_get_args(); - if (is_string($args[0]) && count($args) === 1) { - $args = explode(' ', $args[0]); - } - - $Dispatcher = new ShellDispatcher($args, false); - return $Dispatcher->dispatch(); - } - -/** - * Runs the Shell with the provided argv. - * - * Delegates calls to Tasks and resolves methods inside the class. Commands are looked - * up with the following order: - * - * - Method on the shell. - * - Matching task name. - * - `main()` method. - * - * If a shell implements a `main()` method, all missing method calls will be sent to - * `main()` with the original method name in the argv. - * - * @param string $command The command name to run on this shell. If this argument is empty, - * and the shell has a `main()` method, that will be called instead. - * @param array $argv Array of arguments to run the shell with. This array should be missing the shell name. - * @return int|bool - * @link https://book.cakephp.org/2.0/en/console-and-shells.html#Shell::runCommand - */ - public function runCommand($command, $argv) { - $isTask = $this->hasTask($command); - $isMethod = $this->hasMethod($command); - $isMain = $this->hasMethod('main'); - - if ($isTask || $isMethod && $command !== 'execute') { - array_shift($argv); - } - - $this->OptionParser = $this->getOptionParser(); - try { - list($this->params, $this->args) = $this->OptionParser->parse($argv, $command); - } catch (ConsoleException $e) { - $this->err(__d('cake_console', 'Error: %s', $e->getMessage())); - $this->out($this->OptionParser->help($command)); - return false; - } - - if (!empty($this->params['quiet'])) { - $this->_useLogger(false); - } - if (!empty($this->params['plugin'])) { - CakePlugin::load($this->params['plugin']); - } - $this->command = $command; - if (!empty($this->params['help'])) { - return $this->_displayHelp($command); - } - - if (($isTask || $isMethod || $isMain) && $command !== 'execute') { - $this->startup(); - } - - if ($isTask) { - $command = Inflector::camelize($command); - return $this->{$command}->runCommand('execute', $argv); - } - if ($isMethod) { - return $this->{$command}(); - } - if ($isMain) { - return $this->main(); - } - $this->out($this->OptionParser->help($command)); - return false; - } - -/** - * Display the help in the correct format - * - * @param string $command The command to get help for. - * @return int|bool - */ - protected function _displayHelp($command) { - $format = 'text'; - if (!empty($this->args[0]) && $this->args[0] === 'xml') { - $format = 'xml'; - $this->stdout->outputAs(ConsoleOutput::RAW); - } else { - $this->_welcome(); - } - return $this->out($this->OptionParser->help($command, $format)); - } - -/** - * Gets the option parser instance and configures it. - * - * By overriding this method you can configure the ConsoleOptionParser before returning it. - * - * @return ConsoleOptionParser - * @link https://book.cakephp.org/2.0/en/console-and-shells.html#Shell::getOptionParser - */ - public function getOptionParser() { - $name = ($this->plugin ? $this->plugin . '.' : '') . $this->name; - $parser = new ConsoleOptionParser($name); - return $parser; - } - -/** - * Overload get for lazy building of tasks - * - * @param string $name The property name to access. - * @return Shell Object of Task - */ - public function __get($name) { - if (empty($this->{$name}) && in_array($name, $this->taskNames)) { - $properties = $this->_taskMap[$name]; - $this->{$name} = $this->Tasks->load($properties['class'], $properties['settings']); - $this->{$name}->args =& $this->args; - $this->{$name}->params =& $this->params; - $this->{$name}->initialize(); - $this->{$name}->loadTasks(); - } - return $this->{$name}; - } - -/** - * Safely access the values in $this->params. - * - * @param string $name The name of the parameter to get. - * @return string|bool|null Value. Will return null if it doesn't exist. - */ - public function param($name) { - if (!isset($this->params[$name])) { - return null; - } - return $this->params[$name]; - } - -/** - * Prompts the user for input, and returns it. - * - * @param string $prompt Prompt text. - * @param string|array $options Array or string of options. - * @param string $default Default input value. - * @return mixed Either the default value, or the user-provided input. - * @link https://book.cakephp.org/2.0/en/console-and-shells.html#Shell::in - */ - public function in($prompt, $options = null, $default = null) { - if (!$this->interactive) { - return $default; - } - $originalOptions = $options; - $in = $this->_getInput($prompt, $originalOptions, $default); - - if ($options && is_string($options)) { - if (strpos($options, ',')) { - $options = explode(',', $options); - } elseif (strpos($options, '/')) { - $options = explode('/', $options); - } else { - $options = array($options); - } - } - if (is_array($options)) { - $options = array_merge( - array_map('strtolower', $options), - array_map('strtoupper', $options), - $options - ); - while ($in === '' || !in_array($in, $options)) { - $in = $this->_getInput($prompt, $originalOptions, $default); - } - } - return $in; - } - -/** - * Prompts the user for input, and returns it. - * - * @param string $prompt Prompt text. - * @param string|array $options Array or string of options. - * @param string $default Default input value. - * @return string|int the default value, or the user-provided input. - */ - protected function _getInput($prompt, $options, $default) { - if (!is_array($options)) { - $printOptions = ''; - } else { - $printOptions = '(' . implode('/', $options) . ')'; - } - - if ($default === null) { - $this->stdout->write('' . $prompt . '' . " $printOptions \n" . '> ', 0); - } else { - $this->stdout->write('' . $prompt . '' . " $printOptions \n" . "[$default] > ", 0); - } - $result = $this->stdin->read(); - - if ($result === false) { - $this->_stop(self::CODE_ERROR); - return self::CODE_ERROR; - } - $result = trim($result); - - if ($default !== null && ($result === '' || $result === null)) { - return $default; - } - return $result; - } - -/** - * Wrap a block of text. - * Allows you to set the width, and indenting on a block of text. - * - * ### Options - * - * - `width` The width to wrap to. Defaults to 72 - * - `wordWrap` Only wrap on words breaks (spaces) Defaults to true. - * - `indent` Indent the text with the string provided. Defaults to null. - * - * @param string $text Text the text to format. - * @param string|int|array $options Array of options to use, or an integer to wrap the text to. - * @return string Wrapped / indented text - * @see CakeText::wrap() - * @link https://book.cakephp.org/2.0/en/console-and-shells.html#Shell::wrapText - */ - public function wrapText($text, $options = array()) { - return CakeText::wrap($text, $options); - } - -/** - * Outputs a single or multiple messages to stdout. If no parameters - * are passed outputs just a newline. - * - * ### Output levels - * - * There are 3 built-in output level. Shell::QUIET, Shell::NORMAL, Shell::VERBOSE. - * The verbose and quiet output levels, map to the `verbose` and `quiet` output switches - * present in most shells. Using Shell::QUIET for a message means it will always display. - * While using Shell::VERBOSE means it will only display when verbose output is toggled. - * - * @param string|array $message A string or an array of strings to output - * @param int $newlines Number of newlines to append - * @param int $level The message's output level, see above. - * @return int|bool Returns the number of bytes returned from writing to stdout. - * @link https://book.cakephp.org/2.0/en/console-and-shells.html#Shell::out - */ - public function out($message = null, $newlines = 1, $level = Shell::NORMAL) { - $currentLevel = Shell::NORMAL; - if (!empty($this->params['verbose'])) { - $currentLevel = Shell::VERBOSE; - } - if (!empty($this->params['quiet'])) { - $currentLevel = Shell::QUIET; - } - if ($level <= $currentLevel) { - $this->_lastWritten = $this->stdout->write($message, $newlines); - return $this->_lastWritten; - } - return true; - } - -/** - * Overwrite some already output text. - * - * Useful for building progress bars, or when you want to replace - * text already output to the screen with new text. - * - * **Warning** You cannot overwrite text that contains newlines. - * - * @param array|string $message The message to output. - * @param int $newlines Number of newlines to append. - * @param int $size The number of bytes to overwrite. Defaults to the length of the last message output. - * @return int|bool Returns the number of bytes returned from writing to stdout. - */ - public function overwrite($message, $newlines = 1, $size = null) { - $size = $size ? $size : $this->_lastWritten; - - // Output backspaces. - $this->out(str_repeat("\x08", $size), 0); - - $newBytes = $this->out($message, 0); - - // Fill any remaining bytes with spaces. - $fill = $size - $newBytes; - if ($fill > 0) { - $this->out(str_repeat(' ', $fill), 0); - } - if ($newlines) { - $this->out($this->nl($newlines), 0); - } - } - -/** - * Outputs a single or multiple error messages to stderr. If no parameters - * are passed outputs just a newline. - * - * @param string|array $message A string or an array of strings to output - * @param int $newlines Number of newlines to append - * @return void - * @link https://book.cakephp.org/2.0/en/console-and-shells.html#Shell::err - */ - public function err($message = null, $newlines = 1) { - $this->stderr->write($message, $newlines); - } - -/** - * Returns a single or multiple linefeeds sequences. - * - * @param int $multiplier Number of times the linefeed sequence should be repeated - * @return string - * @link https://book.cakephp.org/2.0/en/console-and-shells.html#Shell::nl - */ - public function nl($multiplier = 1) { - return str_repeat(ConsoleOutput::LF, $multiplier); - } - -/** - * Outputs a series of minus characters to the standard output, acts as a visual separator. - * - * @param int $newlines Number of newlines to pre- and append - * @param int $width Width of the line, defaults to 63 - * @return void - * @link https://book.cakephp.org/2.0/en/console-and-shells.html#Shell::hr - */ - public function hr($newlines = 0, $width = 63) { - $this->out(null, $newlines); - $this->out(str_repeat('-', $width)); - $this->out(null, $newlines); - } - -/** - * Displays a formatted error message - * and exits the application with status code 1 - * - * @param string $title Title of the error - * @param string $message An optional error message - * @return int - * @link https://book.cakephp.org/2.0/en/console-and-shells.html#Shell::error - */ - public function error($title, $message = null) { - $this->err(__d('cake_console', 'Error: %s', $title)); - - if (!empty($message)) { - $this->err($message); - } - $this->_stop(self::CODE_ERROR); - return self::CODE_ERROR; - } - -/** - * Clear the console - * - * @return void - * @link https://book.cakephp.org/2.0/en/console-and-shells.html#Shell::clear - */ - public function clear() { - if (empty($this->params['noclear'])) { - if (DS === '/') { - passthru('clear'); - } else { - passthru('cls'); - } - } - } - -/** - * Creates a file at given path - * - * @param string $path Where to put the file. - * @param string $contents Content to put in the file. - * @return bool Success - * @link https://book.cakephp.org/2.0/en/console-and-shells.html#Shell::createFile - */ - public function createFile($path, $contents) { - $this->out(); - - if (is_file($path) && empty($this->params['force']) && $this->interactive === true) { - $this->out(__d('cake_console', 'File `%s` exists', $path)); - $key = $this->in(__d('cake_console', 'Do you want to overwrite?'), array('y', 'n', 'q'), 'n'); - - if (strtolower($key) === 'q') { - $this->out(__d('cake_console', 'Quitting.'), 2); - $this->_stop(); - return true; - } elseif (strtolower($key) !== 'y') { - $this->out(__d('cake_console', 'Skip `%s`', $path), 2); - return false; - } - } else { - $this->out(__d('cake_console', 'Creating file %s', $path)); - } - - $File = new File($path, true); - if ($File->exists() && $File->writable()) { - $data = $File->prepare($contents); - $File->write($data); - $this->out(__d('cake_console', 'Wrote `%s`', $path)); - return true; - } - - $this->err(__d('cake_console', 'Could not write to `%s`.', $path), 2); - return false; - } - -/** - * Load given shell helper class - * - * @param string $name Name of the helper class. Supports plugin syntax. - * @return BaseShellHelper Instance of helper class - * @throws RuntimeException If invalid class name is provided - */ - public function helper($name) { - if (isset($this->_helpers[$name])) { - return $this->_helpers[$name]; - } - list($plugin, $helperClassName) = pluginSplit($name, true); - $helperClassNameShellHelper = Inflector::camelize($helperClassName) . "ShellHelper"; - App::uses($helperClassNameShellHelper, $plugin . "Console/Helper"); - if (!class_exists($helperClassNameShellHelper)) { - throw new RuntimeException("Class " . $helperClassName . " not found"); - } - $helper = new $helperClassNameShellHelper($this->stdout); - $this->_helpers[$name] = $helper; - return $helper; - } - -/** - * Action to create a Unit Test - * - * @return bool Success - */ - protected function _checkUnitTest() { - if (class_exists('PHPUnit_Framework_TestCase')) { - return true; - //@codingStandardsIgnoreStart - } elseif (@include 'PHPUnit' . DS . 'Autoload.php') { - //@codingStandardsIgnoreEnd - return true; - } elseif (App::import('Vendor', 'phpunit', array('file' => 'PHPUnit' . DS . 'Autoload.php'))) { - return true; - } - - $prompt = __d('cake_console', 'PHPUnit is not installed. Do you want to bake unit test files anyway?'); - $unitTest = $this->in($prompt, array('y', 'n'), 'y'); - $result = strtolower($unitTest) === 'y' || strtolower($unitTest) === 'yes'; - - if ($result) { - $this->out(); - $this->out(__d('cake_console', 'You can download PHPUnit from %s', 'http://phpunit.de')); - } - return $result; - } - -/** - * Makes absolute file path easier to read - * - * @param string $file Absolute file path - * @return string short path - * @link https://book.cakephp.org/2.0/en/console-and-shells.html#Shell::shortPath - */ - public function shortPath($file) { - $shortPath = str_replace(ROOT, null, $file); - $shortPath = str_replace('..' . DS, '', $shortPath); - return str_replace(DS . DS, DS, $shortPath); - } - -/** - * Creates the proper controller path for the specified controller class name - * - * @param string $name Controller class name - * @return string Path to controller - */ - protected function _controllerPath($name) { - return Inflector::underscore($name); - } - -/** - * Creates the proper controller plural name for the specified controller class name - * - * @param string $name Controller class name - * @return string Controller plural name - */ - protected function _controllerName($name) { - return Inflector::pluralize(Inflector::camelize($name)); - } - -/** - * Creates the proper model camelized name (singularized) for the specified name - * - * @param string $name Name - * @return string Camelized and singularized model name - */ - protected function _modelName($name) { - return Inflector::camelize(Inflector::singularize($name)); - } - -/** - * Creates the proper underscored model key for associations - * - * @param string $name Model class name - * @return string Singular model key - */ - protected function _modelKey($name) { - return Inflector::underscore($name) . '_id'; - } - -/** - * Creates the proper model name from a foreign key - * - * @param string $key Foreign key - * @return string Model name - */ - protected function _modelNameFromKey($key) { - return Inflector::camelize(str_replace('_id', '', $key)); - } - -/** - * creates the singular name for use in views. - * - * @param string $name The plural underscored value. - * @return string name - */ - protected function _singularName($name) { - return Inflector::variable(Inflector::singularize($name)); - } - -/** - * Creates the plural name for views - * - * @param string $name Name to use - * @return string Plural name for views - */ - protected function _pluralName($name) { - return Inflector::variable(Inflector::pluralize($name)); - } - -/** - * Creates the singular human name used in views - * - * @param string $name Controller name - * @return string Singular human name - */ - protected function _singularHumanName($name) { - return Inflector::humanize(Inflector::underscore(Inflector::singularize($name))); - } - -/** - * Creates the plural human name used in views - * - * @param string $name Controller name - * @return string Plural human name - */ - protected function _pluralHumanName($name) { - return Inflector::humanize(Inflector::underscore($name)); - } - -/** - * Find the correct path for a plugin. Scans $pluginPaths for the plugin you want. - * - * @param string $pluginName Name of the plugin you want ie. DebugKit - * @return string path path to the correct plugin. - */ - protected function _pluginPath($pluginName) { - if (CakePlugin::loaded($pluginName)) { - return CakePlugin::path($pluginName); - } - return current(App::path('plugins')) . $pluginName . DS; - } - -/** - * Used to enable or disable logging stream output to stdout and stderr - * If you don't wish to see in your stdout or stderr everything that is logged - * through CakeLog, call this function with first param as false - * - * @param bool $enable whether to enable CakeLog output or not - * @return void - */ - protected function _useLogger($enable = true) { - if (!$enable) { - CakeLog::drop('stdout'); - CakeLog::drop('stderr'); - return; - } - if (!$this->_loggerIsConfigured("stdout")) { - $this->_configureStdOutLogger(); - } - if (!$this->_loggerIsConfigured("stderr")) { - $this->_configureStdErrLogger(); - } - } - -/** - * Configure the stdout logger - * - * @return void - */ - protected function _configureStdOutLogger() { - CakeLog::config('stdout', array( - 'engine' => 'Console', - 'types' => array('notice', 'info'), - 'stream' => $this->stdout, - )); - } - -/** - * Configure the stderr logger - * - * @return void - */ - protected function _configureStdErrLogger() { - CakeLog::config('stderr', array( - 'engine' => 'Console', - 'types' => array('emergency', 'alert', 'critical', 'error', 'warning', 'debug'), - 'stream' => $this->stderr, - )); - } - -/** - * Checks if the given logger is configured - * - * @param string $logger The name of the logger to check - * @return bool - */ - protected function _loggerIsConfigured($logger) { - $configured = CakeLog::configured(); - return in_array($logger, $configured); - } +class Shell extends CakeObject +{ + + /** + * Default error code + * + * @var int + */ + const CODE_ERROR = 1; + + /** + * Output constant making verbose shells. + * + * @var int + */ + const VERBOSE = 2; + + /** + * Output constant for making normal shells. + * + * @var int + */ + const NORMAL = 1; + + /** + * Output constants for making quiet shells. + * + * @var int + */ + const QUIET = 0; + + /** + * An instance of ConsoleOptionParser that has been configured for this class. + * + * @var ConsoleOptionParser + */ + public $OptionParser; + + /** + * If true, the script will ask for permission to perform actions. + * + * @var bool + */ + public $interactive = true; + + /** + * Contains command switches parsed from the command line. + * + * @var array + */ + public $params = []; + + /** + * The command (method/task) that is being run. + * + * @var string + */ + public $command; + + /** + * Contains arguments parsed from the command line. + * + * @var array + */ + public $args = []; + + /** + * The name of the shell in camelized. + * + * @var string + */ + public $name = null; + + /** + * The name of the plugin the shell belongs to. + * Is automatically set by ShellDispatcher when a shell is constructed. + * + * @var string + */ + public $plugin = null; + + /** + * Contains tasks to load and instantiate + * + * @var array + * @link https://book.cakephp.org/2.0/en/console-and-shells.html#Shell::$tasks + */ + public $tasks = []; + + /** + * Contains the loaded tasks + * + * @var array + */ + public $taskNames = []; + + /** + * Contains models to load and instantiate + * + * @var array + * @link https://book.cakephp.org/2.0/en/console-and-shells.html#Shell::$uses + */ + public $uses = []; + + /** + * This shell's primary model class name, the first model in the $uses property + * + * @var string + */ + public $modelClass = null; + + /** + * Task Collection for the command, used to create Tasks. + * + * @var TaskCollection + */ + public $Tasks; + /** + * stdout object. + * + * @var ConsoleOutput + */ + public $stdout; + /** + * stderr object. + * + * @var ConsoleOutput + */ + public $stderr; + /** + * stdin object + * + * @var ConsoleInput + */ + public $stdin; + /** + * Normalized map of tasks. + * + * @var string + */ + protected $_taskMap = []; + /** + * The number of bytes last written to the output stream + * used when overwriting the previous message. + * + * @var int + */ + protected $_lastWritten = 0; + + /** + * Contains helpers which have been previously instantiated + * + * @var array + */ + protected $_helpers = []; + + /** + * Constructs this Shell instance. + * + * @param ConsoleOutput $stdout A ConsoleOutput object for stdout. + * @param ConsoleOutput $stderr A ConsoleOutput object for stderr. + * @param ConsoleInput $stdin A ConsoleInput object for stdin. + * @link https://book.cakephp.org/2.0/en/console-and-shells.html#Shell + */ + public function __construct($stdout = null, $stderr = null, $stdin = null) + { + if (!$this->name) { + $this->name = Inflector::camelize(str_replace(['Shell', 'Task'], '', get_class($this))); + } + $this->Tasks = new TaskCollection($this); + + $this->stdout = $stdout ? $stdout : new ConsoleOutput('php://stdout'); + $this->stderr = $stderr ? $stderr : new ConsoleOutput('php://stderr'); + $this->stdin = $stdin ? $stdin : new ConsoleInput('php://stdin'); + + $this->_useLogger(); + $parent = get_parent_class($this); + if ($this->tasks !== null && $this->tasks !== false) { + $this->_mergeVars(['tasks'], $parent, true); + } + if (!empty($this->uses)) { + $this->_mergeVars(['uses'], $parent, false); + } + } + + /** + * Used to enable or disable logging stream output to stdout and stderr + * If you don't wish to see in your stdout or stderr everything that is logged + * through CakeLog, call this function with first param as false + * + * @param bool $enable whether to enable CakeLog output or not + * @return void + */ + protected function _useLogger($enable = true) + { + if (!$enable) { + CakeLog::drop('stdout'); + CakeLog::drop('stderr'); + return; + } + if (!$this->_loggerIsConfigured("stdout")) { + $this->_configureStdOutLogger(); + } + if (!$this->_loggerIsConfigured("stderr")) { + $this->_configureStdErrLogger(); + } + } + + /** + * Checks if the given logger is configured + * + * @param string $logger The name of the logger to check + * @return bool + */ + protected function _loggerIsConfigured($logger) + { + $configured = CakeLog::configured(); + return in_array($logger, $configured); + } + + /** + * Configure the stdout logger + * + * @return void + */ + protected function _configureStdOutLogger() + { + CakeLog::config('stdout', [ + 'engine' => 'Console', + 'types' => ['notice', 'info'], + 'stream' => $this->stdout, + ]); + } + + /** + * Configure the stderr logger + * + * @return void + */ + protected function _configureStdErrLogger() + { + CakeLog::config('stderr', [ + 'engine' => 'Console', + 'types' => ['emergency', 'alert', 'critical', 'error', 'warning', 'debug'], + 'stream' => $this->stderr, + ]); + } + + /** + * Lazy loads models using the loadModel() method if declared in $uses + * + * @param string $name The name of the model to look for. + * @return void + */ + public function __isset($name) + { + if (is_array($this->uses)) { + foreach ($this->uses as $modelClass) { + list(, $class) = pluginSplit($modelClass); + if ($name === $class) { + return $this->loadModel($modelClass); + } + } + } + } + + /** + * Loads and instantiates models required by this shell. + * + * @param string $modelClass Name of model class to load + * @param mixed $id Initial ID the instanced model class should have + * @return mixed true when single model found and instance created, error returned if model not found. + * @throws MissingModelException if the model class cannot be found. + */ + public function loadModel($modelClass = null, $id = null) + { + if ($modelClass === null) { + $modelClass = $this->modelClass; + } + + $this->uses = ($this->uses) ? (array)$this->uses : []; + if (!in_array($modelClass, $this->uses)) { + $this->uses[] = $modelClass; + } + + list($plugin, $modelClass) = pluginSplit($modelClass, true); + if (!isset($this->modelClass)) { + $this->modelClass = $modelClass; + } + + $this->{$modelClass} = ClassRegistry::init([ + 'class' => $plugin . $modelClass, 'alias' => $modelClass, 'id' => $id + ]); + if (!$this->{$modelClass}) { + throw new MissingModelException($modelClass); + } + return true; + } + + /** + * Dispatch a command to another Shell. Similar to CakeObject::requestAction() + * but intended for running shells from other shells. + * + * ### Usage: + * + * With a string command: + * + * `return $this->dispatchShell('schema create DbAcl');` + * + * Avoid using this form if you have string arguments, with spaces in them. + * The dispatched will be invoked incorrectly. Only use this form for simple + * command dispatching. + * + * With an array command: + * + * `return $this->dispatchShell('schema', 'create', 'i18n', '--dry');` + * + * @return mixed The return of the other shell. + * @link https://book.cakephp.org/2.0/en/console-and-shells.html#Shell::dispatchShell + */ + public function dispatchShell() + { + $args = func_get_args(); + if (is_string($args[0]) && count($args) === 1) { + $args = explode(' ', $args[0]); + } + + $Dispatcher = new ShellDispatcher($args, false); + return $Dispatcher->dispatch(); + } + + /** + * Runs the Shell with the provided argv. + * + * Delegates calls to Tasks and resolves methods inside the class. Commands are looked + * up with the following order: + * + * - Method on the shell. + * - Matching task name. + * - `main()` method. + * + * If a shell implements a `main()` method, all missing method calls will be sent to + * `main()` with the original method name in the argv. + * + * @param string $command The command name to run on this shell. If this argument is empty, + * and the shell has a `main()` method, that will be called instead. + * @param array $argv Array of arguments to run the shell with. This array should be missing the shell name. + * @return int|bool + * @link https://book.cakephp.org/2.0/en/console-and-shells.html#Shell::runCommand + */ + public function runCommand($command, $argv) + { + $isTask = $this->hasTask($command); + $isMethod = $this->hasMethod($command); + $isMain = $this->hasMethod('main'); + + if ($isTask || $isMethod && $command !== 'execute') { + array_shift($argv); + } + + $this->OptionParser = $this->getOptionParser(); + try { + list($this->params, $this->args) = $this->OptionParser->parse($argv, $command); + } catch (ConsoleException $e) { + $this->err(__d('cake_console', 'Error: %s', $e->getMessage())); + $this->out($this->OptionParser->help($command)); + return false; + } + + if (!empty($this->params['quiet'])) { + $this->_useLogger(false); + } + if (!empty($this->params['plugin'])) { + CakePlugin::load($this->params['plugin']); + } + $this->command = $command; + if (!empty($this->params['help'])) { + return $this->_displayHelp($command); + } + + if (($isTask || $isMethod || $isMain) && $command !== 'execute') { + $this->startup(); + } + + if ($isTask) { + $command = Inflector::camelize($command); + return $this->{$command}->runCommand('execute', $argv); + } + if ($isMethod) { + return $this->{$command}(); + } + if ($isMain) { + return $this->main(); + } + $this->out($this->OptionParser->help($command)); + return false; + } + + /** + * Check to see if this shell has a task with the provided name. + * + * @param string $task The task name to check. + * @return bool Success + * @link https://book.cakephp.org/2.0/en/console-and-shells.html#Shell::hasTask + */ + public function hasTask($task) + { + return isset($this->_taskMap[Inflector::camelize($task)]); + } + + /** + * Check to see if this shell has a callable method by the given name. + * + * @param string $name The method name to check. + * @return bool + * @link https://book.cakephp.org/2.0/en/console-and-shells.html#Shell::hasMethod + */ + public function hasMethod($name) + { + try { + $method = new ReflectionMethod($this, $name); + if (!$method->isPublic() || substr($name, 0, 1) === '_') { + return false; + } + if ($method->getDeclaringClass()->name === 'Shell') { + return false; + } + return true; + } catch (ReflectionException $e) { + return false; + } + } + + /** + * Gets the option parser instance and configures it. + * + * By overriding this method you can configure the ConsoleOptionParser before returning it. + * + * @return ConsoleOptionParser + * @link https://book.cakephp.org/2.0/en/console-and-shells.html#Shell::getOptionParser + */ + public function getOptionParser() + { + $name = ($this->plugin ? $this->plugin . '.' : '') . $this->name; + $parser = new ConsoleOptionParser($name); + return $parser; + } + + /** + * Outputs a single or multiple error messages to stderr. If no parameters + * are passed outputs just a newline. + * + * @param string|array $message A string or an array of strings to output + * @param int $newlines Number of newlines to append + * @return void + * @link https://book.cakephp.org/2.0/en/console-and-shells.html#Shell::err + */ + public function err($message = null, $newlines = 1) + { + $this->stderr->write($message, $newlines); + } + + /** + * Outputs a single or multiple messages to stdout. If no parameters + * are passed outputs just a newline. + * + * ### Output levels + * + * There are 3 built-in output level. Shell::QUIET, Shell::NORMAL, Shell::VERBOSE. + * The verbose and quiet output levels, map to the `verbose` and `quiet` output switches + * present in most shells. Using Shell::QUIET for a message means it will always display. + * While using Shell::VERBOSE means it will only display when verbose output is toggled. + * + * @param string|array $message A string or an array of strings to output + * @param int $newlines Number of newlines to append + * @param int $level The message's output level, see above. + * @return int|bool Returns the number of bytes returned from writing to stdout. + * @link https://book.cakephp.org/2.0/en/console-and-shells.html#Shell::out + */ + public function out($message = null, $newlines = 1, $level = Shell::NORMAL) + { + $currentLevel = Shell::NORMAL; + if (!empty($this->params['verbose'])) { + $currentLevel = Shell::VERBOSE; + } + if (!empty($this->params['quiet'])) { + $currentLevel = Shell::QUIET; + } + if ($level <= $currentLevel) { + $this->_lastWritten = $this->stdout->write($message, $newlines); + return $this->_lastWritten; + } + return true; + } + + /** + * Display the help in the correct format + * + * @param string $command The command to get help for. + * @return int|bool + */ + protected function _displayHelp($command) + { + $format = 'text'; + if (!empty($this->args[0]) && $this->args[0] === 'xml') { + $format = 'xml'; + $this->stdout->outputAs(ConsoleOutput::RAW); + } else { + $this->_welcome(); + } + return $this->out($this->OptionParser->help($command, $format)); + } + + /** + * Displays a header for the shell + * + * @return void + */ + protected function _welcome() + { + $this->out(); + $this->out(__d('cake_console', 'Welcome to CakePHP %s Console', 'v' . Configure::version())); + $this->hr(); + $this->out(__d('cake_console', 'App : %s', APP_DIR)); + $this->out(__d('cake_console', 'Path: %s', APP)); + $this->hr(); + } + + /** + * Outputs a series of minus characters to the standard output, acts as a visual separator. + * + * @param int $newlines Number of newlines to pre- and append + * @param int $width Width of the line, defaults to 63 + * @return void + * @link https://book.cakephp.org/2.0/en/console-and-shells.html#Shell::hr + */ + public function hr($newlines = 0, $width = 63) + { + $this->out(null, $newlines); + $this->out(str_repeat('-', $width)); + $this->out(null, $newlines); + } + + /** + * Starts up the Shell and displays the welcome message. + * Allows for checking and configuring prior to command or main execution + * + * Override this method if you want to remove the welcome information, + * or otherwise modify the pre-command flow. + * + * @return void + * @link https://book.cakephp.org/2.0/en/console-and-shells.html#Shell::startup + */ + public function startup() + { + $this->_welcome(); + } + + /** + * Overload get for lazy building of tasks + * + * @param string $name The property name to access. + * @return Shell Object of Task + */ + public function __get($name) + { + if (empty($this->{$name}) && in_array($name, $this->taskNames)) { + $properties = $this->_taskMap[$name]; + $this->{$name} = $this->Tasks->load($properties['class'], $properties['settings']); + $this->{$name}->args =& $this->args; + $this->{$name}->params =& $this->params; + $this->{$name}->initialize(); + $this->{$name}->loadTasks(); + } + return $this->{$name}; + } + + /** + * Initializes the Shell + * acts as constructor for subclasses + * allows configuration of tasks prior to shell execution + * + * @return void + * @link https://book.cakephp.org/2.0/en/console-and-shells.html#Shell::initialize + */ + public function initialize() + { + $this->_loadModels(); + $this->loadTasks(); + } + + /** + * If $uses is an array load each of the models in the array + * + * @return bool + */ + protected function _loadModels() + { + if (is_array($this->uses)) { + list(, $this->modelClass) = pluginSplit(current($this->uses)); + foreach ($this->uses as $modelClass) { + $this->loadModel($modelClass); + } + } + return true; + } + + /** + * Loads tasks defined in public $tasks + * + * @return bool + */ + public function loadTasks() + { + if ($this->tasks === true || empty($this->tasks) || empty($this->Tasks)) { + return true; + } + $this->_taskMap = TaskCollection::normalizeObjectArray((array)$this->tasks); + $this->taskNames = array_merge($this->taskNames, array_keys($this->_taskMap)); + return true; + } + + /** + * Safely access the values in $this->params. + * + * @param string $name The name of the parameter to get. + * @return string|bool|null Value. Will return null if it doesn't exist. + */ + public function param($name) + { + if (!isset($this->params[$name])) { + return null; + } + return $this->params[$name]; + } + + /** + * Wrap a block of text. + * Allows you to set the width, and indenting on a block of text. + * + * ### Options + * + * - `width` The width to wrap to. Defaults to 72 + * - `wordWrap` Only wrap on words breaks (spaces) Defaults to true. + * - `indent` Indent the text with the string provided. Defaults to null. + * + * @param string $text Text the text to format. + * @param string|int|array $options Array of options to use, or an integer to wrap the text to. + * @return string Wrapped / indented text + * @see CakeText::wrap() + * @link https://book.cakephp.org/2.0/en/console-and-shells.html#Shell::wrapText + */ + public function wrapText($text, $options = []) + { + return CakeText::wrap($text, $options); + } + + /** + * Overwrite some already output text. + * + * Useful for building progress bars, or when you want to replace + * text already output to the screen with new text. + * + * **Warning** You cannot overwrite text that contains newlines. + * + * @param array|string $message The message to output. + * @param int $newlines Number of newlines to append. + * @param int $size The number of bytes to overwrite. Defaults to the length of the last message output. + * @return int|bool Returns the number of bytes returned from writing to stdout. + */ + public function overwrite($message, $newlines = 1, $size = null) + { + $size = $size ? $size : $this->_lastWritten; + + // Output backspaces. + $this->out(str_repeat("\x08", $size), 0); + + $newBytes = $this->out($message, 0); + + // Fill any remaining bytes with spaces. + $fill = $size - $newBytes; + if ($fill > 0) { + $this->out(str_repeat(' ', $fill), 0); + } + if ($newlines) { + $this->out($this->nl($newlines), 0); + } + } + + /** + * Returns a single or multiple linefeeds sequences. + * + * @param int $multiplier Number of times the linefeed sequence should be repeated + * @return string + * @link https://book.cakephp.org/2.0/en/console-and-shells.html#Shell::nl + */ + public function nl($multiplier = 1) + { + return str_repeat(ConsoleOutput::LF, $multiplier); + } + + /** + * Displays a formatted error message + * and exits the application with status code 1 + * + * @param string $title Title of the error + * @param string $message An optional error message + * @return int + * @link https://book.cakephp.org/2.0/en/console-and-shells.html#Shell::error + */ + public function error($title, $message = null) + { + $this->err(__d('cake_console', 'Error: %s', $title)); + + if (!empty($message)) { + $this->err($message); + } + $this->_stop(self::CODE_ERROR); + return self::CODE_ERROR; + } + + /** + * Clear the console + * + * @return void + * @link https://book.cakephp.org/2.0/en/console-and-shells.html#Shell::clear + */ + public function clear() + { + if (empty($this->params['noclear'])) { + if (DS === '/') { + passthru('clear'); + } else { + passthru('cls'); + } + } + } + + /** + * Creates a file at given path + * + * @param string $path Where to put the file. + * @param string $contents Content to put in the file. + * @return bool Success + * @link https://book.cakephp.org/2.0/en/console-and-shells.html#Shell::createFile + */ + public function createFile($path, $contents) + { + $this->out(); + + if (is_file($path) && empty($this->params['force']) && $this->interactive === true) { + $this->out(__d('cake_console', 'File `%s` exists', $path)); + $key = $this->in(__d('cake_console', 'Do you want to overwrite?'), ['y', 'n', 'q'], 'n'); + + if (strtolower($key) === 'q') { + $this->out(__d('cake_console', 'Quitting.'), 2); + $this->_stop(); + return true; + } else if (strtolower($key) !== 'y') { + $this->out(__d('cake_console', 'Skip `%s`', $path), 2); + return false; + } + } else { + $this->out(__d('cake_console', 'Creating file %s', $path)); + } + + $File = new File($path, true); + if ($File->exists() && $File->writable()) { + $data = $File->prepare($contents); + $File->write($data); + $this->out(__d('cake_console', 'Wrote `%s`', $path)); + return true; + } + + $this->err(__d('cake_console', 'Could not write to `%s`.', $path), 2); + return false; + } + + /** + * Prompts the user for input, and returns it. + * + * @param string $prompt Prompt text. + * @param string|array $options Array or string of options. + * @param string $default Default input value. + * @return mixed Either the default value, or the user-provided input. + * @link https://book.cakephp.org/2.0/en/console-and-shells.html#Shell::in + */ + public function in($prompt, $options = null, $default = null) + { + if (!$this->interactive) { + return $default; + } + $originalOptions = $options; + $in = $this->_getInput($prompt, $originalOptions, $default); + + if ($options && is_string($options)) { + if (strpos($options, ',')) { + $options = explode(',', $options); + } else if (strpos($options, '/')) { + $options = explode('/', $options); + } else { + $options = [$options]; + } + } + if (is_array($options)) { + $options = array_merge( + array_map('strtolower', $options), + array_map('strtoupper', $options), + $options + ); + while ($in === '' || !in_array($in, $options)) { + $in = $this->_getInput($prompt, $originalOptions, $default); + } + } + return $in; + } + + /** + * Prompts the user for input, and returns it. + * + * @param string $prompt Prompt text. + * @param string|array $options Array or string of options. + * @param string $default Default input value. + * @return string|int the default value, or the user-provided input. + */ + protected function _getInput($prompt, $options, $default) + { + if (!is_array($options)) { + $printOptions = ''; + } else { + $printOptions = '(' . implode('/', $options) . ')'; + } + + if ($default === null) { + $this->stdout->write('' . $prompt . '' . " $printOptions \n" . '> ', 0); + } else { + $this->stdout->write('' . $prompt . '' . " $printOptions \n" . "[$default] > ", 0); + } + $result = $this->stdin->read(); + + if ($result === false) { + $this->_stop(self::CODE_ERROR); + return self::CODE_ERROR; + } + $result = trim($result); + + if ($default !== null && ($result === '' || $result === null)) { + return $default; + } + return $result; + } + + /** + * Load given shell helper class + * + * @param string $name Name of the helper class. Supports plugin syntax. + * @return BaseShellHelper Instance of helper class + * @throws RuntimeException If invalid class name is provided + */ + public function helper($name) + { + if (isset($this->_helpers[$name])) { + return $this->_helpers[$name]; + } + list($plugin, $helperClassName) = pluginSplit($name, true); + $helperClassNameShellHelper = Inflector::camelize($helperClassName) . "ShellHelper"; + App::uses($helperClassNameShellHelper, $plugin . "Console/Helper"); + if (!class_exists($helperClassNameShellHelper)) { + throw new RuntimeException("Class " . $helperClassName . " not found"); + } + $helper = new $helperClassNameShellHelper($this->stdout); + $this->_helpers[$name] = $helper; + return $helper; + } + + /** + * Makes absolute file path easier to read + * + * @param string $file Absolute file path + * @return string short path + * @link https://book.cakephp.org/2.0/en/console-and-shells.html#Shell::shortPath + */ + public function shortPath($file) + { + $shortPath = str_replace(ROOT, null, $file); + $shortPath = str_replace('..' . DS, '', $shortPath); + return str_replace(DS . DS, DS, $shortPath); + } + + /** + * Action to create a Unit Test + * + * @return bool Success + */ + protected function _checkUnitTest() + { + if (class_exists('PHPUnit_Framework_TestCase')) { + return true; + //@codingStandardsIgnoreStart + } else if (@include 'PHPUnit' . DS . 'Autoload.php') { + //@codingStandardsIgnoreEnd + return true; + } else if (App::import('Vendor', 'phpunit', ['file' => 'PHPUnit' . DS . 'Autoload.php'])) { + return true; + } + + $prompt = __d('cake_console', 'PHPUnit is not installed. Do you want to bake unit test files anyway?'); + $unitTest = $this->in($prompt, ['y', 'n'], 'y'); + $result = strtolower($unitTest) === 'y' || strtolower($unitTest) === 'yes'; + + if ($result) { + $this->out(); + $this->out(__d('cake_console', 'You can download PHPUnit from %s', 'http://phpunit.de')); + } + return $result; + } + + /** + * Creates the proper controller path for the specified controller class name + * + * @param string $name Controller class name + * @return string Path to controller + */ + protected function _controllerPath($name) + { + return Inflector::underscore($name); + } + + /** + * Creates the proper controller plural name for the specified controller class name + * + * @param string $name Controller class name + * @return string Controller plural name + */ + protected function _controllerName($name) + { + return Inflector::pluralize(Inflector::camelize($name)); + } + + /** + * Creates the proper model camelized name (singularized) for the specified name + * + * @param string $name Name + * @return string Camelized and singularized model name + */ + protected function _modelName($name) + { + return Inflector::camelize(Inflector::singularize($name)); + } + + /** + * Creates the proper underscored model key for associations + * + * @param string $name Model class name + * @return string Singular model key + */ + protected function _modelKey($name) + { + return Inflector::underscore($name) . '_id'; + } + + /** + * Creates the proper model name from a foreign key + * + * @param string $key Foreign key + * @return string Model name + */ + protected function _modelNameFromKey($key) + { + return Inflector::camelize(str_replace('_id', '', $key)); + } + + /** + * creates the singular name for use in views. + * + * @param string $name The plural underscored value. + * @return string name + */ + protected function _singularName($name) + { + return Inflector::variable(Inflector::singularize($name)); + } + + /** + * Creates the plural name for views + * + * @param string $name Name to use + * @return string Plural name for views + */ + protected function _pluralName($name) + { + return Inflector::variable(Inflector::pluralize($name)); + } + + /** + * Creates the singular human name used in views + * + * @param string $name Controller name + * @return string Singular human name + */ + protected function _singularHumanName($name) + { + return Inflector::humanize(Inflector::underscore(Inflector::singularize($name))); + } + + /** + * Creates the plural human name used in views + * + * @param string $name Controller name + * @return string Plural human name + */ + protected function _pluralHumanName($name) + { + return Inflector::humanize(Inflector::underscore($name)); + } + + /** + * Find the correct path for a plugin. Scans $pluginPaths for the plugin you want. + * + * @param string $pluginName Name of the plugin you want ie. DebugKit + * @return string path path to the correct plugin. + */ + protected function _pluginPath($pluginName) + { + if (CakePlugin::loaded($pluginName)) { + return CakePlugin::path($pluginName); + } + return current(App::path('plugins')) . $pluginName . DS; + } } diff --git a/lib/Cake/Console/ShellDispatcher.php b/lib/Cake/Console/ShellDispatcher.php index 22a4e53f..87e06b7c 100755 --- a/lib/Cake/Console/ShellDispatcher.php +++ b/lib/Cake/Console/ShellDispatcher.php @@ -20,389 +20,405 @@ * * @package Cake.Console */ -class ShellDispatcher { - -/** - * Contains command switches parsed from the command line. - * - * @var array - */ - public $params = array(); - -/** - * Contains arguments parsed from the command line. - * - * @var array - */ - public $args = array(); - -/** - * Constructor - * - * The execution of the script is stopped after dispatching the request with - * a status code of either 0 or 1 according to the result of the dispatch. - * - * @param array $args the argv from PHP - * @param bool $bootstrap Should the environment be bootstrapped. - */ - public function __construct($args = array(), $bootstrap = true) { - set_time_limit(0); - $this->parseParams($args); - - if ($bootstrap) { - $this->_initConstants(); - $this->_initEnvironment(); - } - } - -/** - * Run the dispatcher - * - * @param array $argv The argv from PHP - * @return void - */ - public static function run($argv) { - $dispatcher = new ShellDispatcher($argv); - return $dispatcher->_stop($dispatcher->dispatch() === false ? 1 : 0); - } - -/** - * Defines core configuration. - * - * @return void - */ - protected function _initConstants() { - if (function_exists('ini_set')) { - ini_set('html_errors', false); - ini_set('implicit_flush', true); - ini_set('max_execution_time', 0); - } - - if (!defined('CAKE_CORE_INCLUDE_PATH')) { - define('CAKE_CORE_INCLUDE_PATH', dirname(dirname(dirname(__FILE__)))); - define('CAKEPHP_SHELL', true); - if (!defined('DS')) { - define('DS', DIRECTORY_SEPARATOR); - } - if (!defined('CORE_PATH')) { - define('CORE_PATH', CAKE_CORE_INCLUDE_PATH . DS); - } - } - } - -/** - * Defines current working environment. - * - * @return void - * @throws CakeException - */ - protected function _initEnvironment() { - if (!$this->_bootstrap()) { - $message = "Unable to load CakePHP core.\nMake sure " . DS . 'lib' . DS . 'Cake exists in ' . CAKE_CORE_INCLUDE_PATH; - throw new CakeException($message); - } - - if (!isset($this->args[0]) || !isset($this->params['working'])) { - $message = "This file has been loaded incorrectly and cannot continue.\n" . - "Please make sure that " . DS . 'lib' . DS . 'Cake' . DS . "Console is in your system path,\n" . - "and check the cookbook for the correct usage of this command.\n" . - "(https://book.cakephp.org/)"; - throw new CakeException($message); - } - - $this->shiftArgs(); - } - -/** - * Initializes the environment and loads the CakePHP core. - * - * @return bool Success. - */ - protected function _bootstrap() { - if (!defined('ROOT')) { - define('ROOT', $this->params['root']); - } - if (!defined('APP_DIR')) { - define('APP_DIR', $this->params['app']); - } - if (!defined('APP')) { - define('APP', $this->params['working'] . DS); - } - if (!defined('WWW_ROOT')) { - if (!$this->_isAbsolutePath($this->params['webroot'])) { - $webroot = realpath(APP . $this->params['webroot']); - } else { - $webroot = $this->params['webroot']; - } - define('WWW_ROOT', $webroot . DS); - } - if (!defined('TMP') && !is_dir(APP . 'tmp')) { - define('TMP', CAKE_CORE_INCLUDE_PATH . DS . 'Cake' . DS . 'Console' . DS . 'Templates' . DS . 'skel' . DS . 'tmp' . DS); - } - - if (!defined('CONFIG')) { - define('CONFIG', ROOT . DS . APP_DIR . DS . 'Config' . DS); - } - // $boot is used by Cake/bootstrap.php file - $boot = file_exists(CONFIG . 'bootstrap.php'); - require CORE_PATH . 'Cake' . DS . 'bootstrap.php'; - - if (!file_exists(CONFIG . 'core.php')) { - include_once CAKE_CORE_INCLUDE_PATH . DS . 'Cake' . DS . 'Console' . DS . 'Templates' . DS . 'skel' . DS . 'Config' . DS . 'core.php'; - App::build(); - } - - $this->setErrorHandlers(); - - if (!defined('FULL_BASE_URL')) { - $url = Configure::read('App.fullBaseUrl'); - define('FULL_BASE_URL', $url ? $url : 'http://localhost'); - Configure::write('App.fullBaseUrl', FULL_BASE_URL); - } - - return true; - } - -/** - * Set the error/exception handlers for the console - * based on the `Error.consoleHandler`, and `Exception.consoleHandler` values - * if they are set. If they are not set, the default ConsoleErrorHandler will be - * used. - * - * @return void - */ - public function setErrorHandlers() { - App::uses('ConsoleErrorHandler', 'Console'); - $error = Configure::read('Error'); - $exception = Configure::read('Exception'); - - $errorHandler = new ConsoleErrorHandler(); - if (empty($error['consoleHandler'])) { - $error['consoleHandler'] = array($errorHandler, 'handleError'); - Configure::write('Error', $error); - } - if (empty($exception['consoleHandler'])) { - $exception['consoleHandler'] = array($errorHandler, 'handleException'); - Configure::write('Exception', $exception); - } - set_exception_handler($exception['consoleHandler']); - set_error_handler($error['consoleHandler'], Configure::read('Error.level')); - - App::uses('Debugger', 'Utility'); - Debugger::getInstance()->output('txt'); - } - -/** - * Dispatches a CLI request - * - * @return bool - * @throws MissingShellMethodException - */ - public function dispatch() { - $shell = $this->shiftArgs(); - - if (!$shell) { - $this->help(); - return false; - } - if (in_array($shell, array('help', '--help', '-h'))) { - $this->help(); - return true; - } - - $Shell = $this->_getShell($shell); - - $command = null; - if (isset($this->args[0])) { - $command = $this->args[0]; - } - - if ($Shell instanceof Shell) { - $Shell->initialize(); - return $Shell->runCommand($command, $this->args); - } - $methods = array_diff(get_class_methods($Shell), get_class_methods('Shell')); - $added = in_array($command, $methods); - $private = substr($command, 0, 1) === '_' && method_exists($Shell, $command); - - if (!$private) { - if ($added) { - $this->shiftArgs(); - $Shell->startup(); - return $Shell->{$command}(); - } - if (method_exists($Shell, 'main')) { - $Shell->startup(); - return $Shell->main(); - } - } - - throw new MissingShellMethodException(array('shell' => $shell, 'method' => $command)); - } - -/** - * Get shell to use, either plugin shell or application shell - * - * All paths in the loaded shell paths are searched. - * - * @param string $shell Optionally the name of a plugin - * @return mixed An object - * @throws MissingShellException when errors are encountered. - */ - protected function _getShell($shell) { - list($plugin, $shell) = pluginSplit($shell, true); - - $plugin = Inflector::camelize($plugin); - $class = Inflector::camelize($shell) . 'Shell'; - - App::uses('Shell', 'Console'); - App::uses('AppShell', 'Console/Command'); - App::uses($class, $plugin . 'Console/Command'); - - if (!class_exists($class)) { - $plugin = Inflector::camelize($shell) . '.'; - App::uses($class, $plugin . 'Console/Command'); - } - - if (!class_exists($class)) { - throw new MissingShellException(array( - 'class' => $class - )); - } - $Shell = new $class(); - $Shell->plugin = trim($plugin, '.'); - return $Shell; - } - -/** - * Parses command line options and extracts the directory paths from $params - * - * @param array $args Parameters to parse - * @return void - */ - public function parseParams($args) { - $this->_parsePaths($args); - - $defaults = array( - 'app' => 'app', - 'root' => dirname(dirname(dirname(dirname(__FILE__)))), - 'working' => null, - 'webroot' => 'webroot' - ); - $params = array_merge($defaults, array_intersect_key($this->params, $defaults)); - $isWin = false; - foreach ($defaults as $default => $value) { - if (strpos($params[$default], '\\') !== false) { - $isWin = true; - break; - } - } - $params = str_replace('\\', '/', $params); - - if (isset($params['working'])) { - $params['working'] = trim($params['working']); - } - - if (!empty($params['working']) && (!isset($this->args[0]) || isset($this->args[0]) && $this->args[0][0] !== '.')) { - if ($params['working'][0] === '.') { - $params['working'] = realpath($params['working']); - } - if (empty($this->params['app']) && $params['working'] != $params['root']) { - $params['root'] = dirname($params['working']); - $params['app'] = basename($params['working']); - } else { - $params['root'] = $params['working']; - } - } - - if ($this->_isAbsolutePath($params['app'])) { - $params['root'] = dirname($params['app']); - } elseif (strpos($params['app'], '/')) { - $params['root'] .= '/' . dirname($params['app']); - } - $isWindowsAppPath = $this->_isWindowsPath($params['app']); - $params['app'] = basename($params['app']); - $params['working'] = rtrim($params['root'], '/'); - if (!$isWin || !preg_match('/^[A-Z]:$/i', $params['app'])) { - $params['working'] .= '/' . $params['app']; - } - - if ($isWindowsAppPath || !empty($isWin)) { - $params = str_replace('/', '\\', $params); - } - - $this->params = $params + $this->params; - } - -/** - * Checks whether the given path is absolute or relative. - * - * @param string $path absolute or relative path. - * @return bool - */ - protected function _isAbsolutePath($path) { - return $path[0] === '/' || $this->_isWindowsPath($path); - } - -/** - * Checks whether the given path is Window OS path. - * - * @param string $path absolute path. - * @return bool - */ - protected function _isWindowsPath($path) { - return preg_match('/([a-z])(:)/i', $path) == 1; - } - -/** - * Parses out the paths from from the argv - * - * @param array $args The argv to parse. - * @return void - */ - protected function _parsePaths($args) { - $parsed = array(); - $keys = array('-working', '--working', '-app', '--app', '-root', '--root', '-webroot', '--webroot'); - $args = (array)$args; - foreach ($keys as $key) { - while (($index = array_search($key, $args)) !== false) { - $keyname = str_replace('-', '', $key); - $valueIndex = $index + 1; - $parsed[$keyname] = $args[$valueIndex]; - array_splice($args, $index, 2); - } - } - $this->args = $args; - $this->params = $parsed; - } - -/** - * Removes first argument and shifts other arguments up - * - * @return mixed Null if there are no arguments otherwise the shifted argument - */ - public function shiftArgs() { - return array_shift($this->args); - } - -/** - * Shows console help. Performs an internal dispatch to the CommandList Shell - * - * @return void - */ - public function help() { - $this->args = array_merge(array('command_list'), $this->args); - $this->dispatch(); - } - -/** - * Stop execution of the current script - * - * @param int|string $status see http://php.net/exit for values - * @return void - */ - protected function _stop($status = 0) { - exit($status); - } +class ShellDispatcher +{ + + /** + * Contains command switches parsed from the command line. + * + * @var array + */ + public $params = []; + + /** + * Contains arguments parsed from the command line. + * + * @var array + */ + public $args = []; + + /** + * Constructor + * + * The execution of the script is stopped after dispatching the request with + * a status code of either 0 or 1 according to the result of the dispatch. + * + * @param array $args the argv from PHP + * @param bool $bootstrap Should the environment be bootstrapped. + */ + public function __construct($args = [], $bootstrap = true) + { + set_time_limit(0); + $this->parseParams($args); + + if ($bootstrap) { + $this->_initConstants(); + $this->_initEnvironment(); + } + } + + /** + * Parses command line options and extracts the directory paths from $params + * + * @param array $args Parameters to parse + * @return void + */ + public function parseParams($args) + { + $this->_parsePaths($args); + + $defaults = [ + 'app' => 'app', + 'root' => dirname(dirname(dirname(dirname(__FILE__)))), + 'working' => null, + 'webroot' => 'webroot' + ]; + $params = array_merge($defaults, array_intersect_key($this->params, $defaults)); + $isWin = false; + foreach ($defaults as $default => $value) { + if (strpos($params[$default], '\\') !== false) { + $isWin = true; + break; + } + } + $params = str_replace('\\', '/', $params); + + if (isset($params['working'])) { + $params['working'] = trim($params['working']); + } + + if (!empty($params['working']) && (!isset($this->args[0]) || isset($this->args[0]) && $this->args[0][0] !== '.')) { + if ($params['working'][0] === '.') { + $params['working'] = realpath($params['working']); + } + if (empty($this->params['app']) && $params['working'] != $params['root']) { + $params['root'] = dirname($params['working']); + $params['app'] = basename($params['working']); + } else { + $params['root'] = $params['working']; + } + } + + if ($this->_isAbsolutePath($params['app'])) { + $params['root'] = dirname($params['app']); + } else if (strpos($params['app'], '/')) { + $params['root'] .= '/' . dirname($params['app']); + } + $isWindowsAppPath = $this->_isWindowsPath($params['app']); + $params['app'] = basename($params['app']); + $params['working'] = rtrim($params['root'], '/'); + if (!$isWin || !preg_match('/^[A-Z]:$/i', $params['app'])) { + $params['working'] .= '/' . $params['app']; + } + + if ($isWindowsAppPath || !empty($isWin)) { + $params = str_replace('/', '\\', $params); + } + + $this->params = $params + $this->params; + } + + /** + * Parses out the paths from from the argv + * + * @param array $args The argv to parse. + * @return void + */ + protected function _parsePaths($args) + { + $parsed = []; + $keys = ['-working', '--working', '-app', '--app', '-root', '--root', '-webroot', '--webroot']; + $args = (array)$args; + foreach ($keys as $key) { + while (($index = array_search($key, $args)) !== false) { + $keyname = str_replace('-', '', $key); + $valueIndex = $index + 1; + $parsed[$keyname] = $args[$valueIndex]; + array_splice($args, $index, 2); + } + } + $this->args = $args; + $this->params = $parsed; + } + + /** + * Checks whether the given path is absolute or relative. + * + * @param string $path absolute or relative path. + * @return bool + */ + protected function _isAbsolutePath($path) + { + return $path[0] === '/' || $this->_isWindowsPath($path); + } + + /** + * Checks whether the given path is Window OS path. + * + * @param string $path absolute path. + * @return bool + */ + protected function _isWindowsPath($path) + { + return preg_match('/([a-z])(:)/i', $path) == 1; + } + + /** + * Defines core configuration. + * + * @return void + */ + protected function _initConstants() + { + if (function_exists('ini_set')) { + ini_set('html_errors', false); + ini_set('implicit_flush', true); + ini_set('max_execution_time', 0); + } + + if (!defined('CAKE_CORE_INCLUDE_PATH')) { + define('CAKE_CORE_INCLUDE_PATH', dirname(dirname(dirname(__FILE__)))); + define('CAKEPHP_SHELL', true); + if (!defined('DS')) { + define('DS', DIRECTORY_SEPARATOR); + } + if (!defined('CORE_PATH')) { + define('CORE_PATH', CAKE_CORE_INCLUDE_PATH . DS); + } + } + } + + /** + * Defines current working environment. + * + * @return void + * @throws CakeException + */ + protected function _initEnvironment() + { + if (!$this->_bootstrap()) { + $message = "Unable to load CakePHP core.\nMake sure " . DS . 'lib' . DS . 'Cake exists in ' . CAKE_CORE_INCLUDE_PATH; + throw new CakeException($message); + } + + if (!isset($this->args[0]) || !isset($this->params['working'])) { + $message = "This file has been loaded incorrectly and cannot continue.\n" . + "Please make sure that " . DS . 'lib' . DS . 'Cake' . DS . "Console is in your system path,\n" . + "and check the cookbook for the correct usage of this command.\n" . + "(https://book.cakephp.org/)"; + throw new CakeException($message); + } + + $this->shiftArgs(); + } + + /** + * Initializes the environment and loads the CakePHP core. + * + * @return bool Success. + */ + protected function _bootstrap() + { + if (!defined('ROOT')) { + define('ROOT', $this->params['root']); + } + if (!defined('APP_DIR')) { + define('APP_DIR', $this->params['app']); + } + if (!defined('APP')) { + define('APP', $this->params['working'] . DS); + } + if (!defined('WWW_ROOT')) { + if (!$this->_isAbsolutePath($this->params['webroot'])) { + $webroot = realpath(APP . $this->params['webroot']); + } else { + $webroot = $this->params['webroot']; + } + define('WWW_ROOT', $webroot . DS); + } + if (!defined('TMP') && !is_dir(APP . 'tmp')) { + define('TMP', CAKE_CORE_INCLUDE_PATH . DS . 'Cake' . DS . 'Console' . DS . 'Templates' . DS . 'skel' . DS . 'tmp' . DS); + } + + if (!defined('CONFIG')) { + define('CONFIG', ROOT . DS . APP_DIR . DS . 'Config' . DS); + } + // $boot is used by Cake/bootstrap.php file + $boot = file_exists(CONFIG . 'bootstrap.php'); + require CORE_PATH . 'Cake' . DS . 'bootstrap.php'; + + if (!file_exists(CONFIG . 'core.php')) { + include_once CAKE_CORE_INCLUDE_PATH . DS . 'Cake' . DS . 'Console' . DS . 'Templates' . DS . 'skel' . DS . 'Config' . DS . 'core.php'; + App::build(); + } + + $this->setErrorHandlers(); + + if (!defined('FULL_BASE_URL')) { + $url = Configure::read('App.fullBaseUrl'); + define('FULL_BASE_URL', $url ? $url : 'http://localhost'); + Configure::write('App.fullBaseUrl', FULL_BASE_URL); + } + + return true; + } + + /** + * Set the error/exception handlers for the console + * based on the `Error.consoleHandler`, and `Exception.consoleHandler` values + * if they are set. If they are not set, the default ConsoleErrorHandler will be + * used. + * + * @return void + */ + public function setErrorHandlers() + { + App::uses('ConsoleErrorHandler', 'Console'); + $error = Configure::read('Error'); + $exception = Configure::read('Exception'); + + $errorHandler = new ConsoleErrorHandler(); + if (empty($error['consoleHandler'])) { + $error['consoleHandler'] = [$errorHandler, 'handleError']; + Configure::write('Error', $error); + } + if (empty($exception['consoleHandler'])) { + $exception['consoleHandler'] = [$errorHandler, 'handleException']; + Configure::write('Exception', $exception); + } + set_exception_handler($exception['consoleHandler']); + set_error_handler($error['consoleHandler'], Configure::read('Error.level')); + + App::uses('Debugger', 'Utility'); + Debugger::getInstance()->output('txt'); + } + + /** + * Removes first argument and shifts other arguments up + * + * @return mixed Null if there are no arguments otherwise the shifted argument + */ + public function shiftArgs() + { + return array_shift($this->args); + } + + /** + * Run the dispatcher + * + * @param array $argv The argv from PHP + * @return void + */ + public static function run($argv) + { + $dispatcher = new ShellDispatcher($argv); + return $dispatcher->_stop($dispatcher->dispatch() === false ? 1 : 0); + } + + /** + * Stop execution of the current script + * + * @param int|string $status see http://php.net/exit for values + * @return void + */ + protected function _stop($status = 0) + { + exit($status); + } + + /** + * Dispatches a CLI request + * + * @return bool + * @throws MissingShellMethodException + */ + public function dispatch() + { + $shell = $this->shiftArgs(); + + if (!$shell) { + $this->help(); + return false; + } + if (in_array($shell, ['help', '--help', '-h'])) { + $this->help(); + return true; + } + + $Shell = $this->_getShell($shell); + + $command = null; + if (isset($this->args[0])) { + $command = $this->args[0]; + } + + if ($Shell instanceof Shell) { + $Shell->initialize(); + return $Shell->runCommand($command, $this->args); + } + $methods = array_diff(get_class_methods($Shell), get_class_methods('Shell')); + $added = in_array($command, $methods); + $private = substr($command, 0, 1) === '_' && method_exists($Shell, $command); + + if (!$private) { + if ($added) { + $this->shiftArgs(); + $Shell->startup(); + return $Shell->{$command}(); + } + if (method_exists($Shell, 'main')) { + $Shell->startup(); + return $Shell->main(); + } + } + + throw new MissingShellMethodException(['shell' => $shell, 'method' => $command]); + } + + /** + * Shows console help. Performs an internal dispatch to the CommandList Shell + * + * @return void + */ + public function help() + { + $this->args = array_merge(['command_list'], $this->args); + $this->dispatch(); + } + + /** + * Get shell to use, either plugin shell or application shell + * + * All paths in the loaded shell paths are searched. + * + * @param string $shell Optionally the name of a plugin + * @return mixed An object + * @throws MissingShellException when errors are encountered. + */ + protected function _getShell($shell) + { + list($plugin, $shell) = pluginSplit($shell, true); + + $plugin = Inflector::camelize($plugin); + $class = Inflector::camelize($shell) . 'Shell'; + + App::uses('Shell', 'Console'); + App::uses('AppShell', 'Console/Command'); + App::uses($class, $plugin . 'Console/Command'); + + if (!class_exists($class)) { + $plugin = Inflector::camelize($shell) . '.'; + App::uses($class, $plugin . 'Console/Command'); + } + + if (!class_exists($class)) { + throw new MissingShellException([ + 'class' => $class + ]); + } + $Shell = new $class(); + $Shell->plugin = trim($plugin, '.'); + return $Shell; + } } diff --git a/lib/Cake/Console/TaskCollection.php b/lib/Cake/Console/TaskCollection.php index 5d2e7a7a..804e8949 100755 --- a/lib/Cake/Console/TaskCollection.php +++ b/lib/Cake/Console/TaskCollection.php @@ -24,77 +24,79 @@ * * @package Cake.Console */ -class TaskCollection extends ObjectCollection { +class TaskCollection extends ObjectCollection +{ -/** - * Shell to use to set params to tasks. - * - * @var Shell - */ - protected $_Shell; - -/** - * The directory inside each shell path that contains tasks. - * - * @var string - */ - public $taskPathPrefix = 'tasks/'; + /** + * The directory inside each shell path that contains tasks. + * + * @var string + */ + public $taskPathPrefix = 'tasks/'; + /** + * Shell to use to set params to tasks. + * + * @var Shell + */ + protected $_Shell; -/** - * Constructor - * - * @param Shell $Shell The shell this task collection is attached to. - */ - public function __construct(Shell $Shell) { - $this->_Shell = $Shell; - } + /** + * Constructor + * + * @param Shell $Shell The shell this task collection is attached to. + */ + public function __construct(Shell $Shell) + { + $this->_Shell = $Shell; + } -/** - * Loads/constructs a task. Will return the instance in the registry if it already exists. - * - * You can alias your task as an existing task by setting the 'className' key, i.e., - * ``` - * public $tasks = array( - * 'DbConfig' => array( - * 'className' => 'Bakeplus.DbConfigure' - * ); - * ); - * ``` - * All calls to the `DbConfig` task would use `DbConfigure` found in the `Bakeplus` plugin instead. - * - * @param string $task Task name to load - * @param array $settings Settings for the task. - * @return AppShell A task object, Either the existing loaded task or a new one. - * @throws MissingTaskException when the task could not be found - */ - public function load($task, $settings = array()) { - if (is_array($settings) && isset($settings['className'])) { - $alias = $task; - $task = $settings['className']; - } - list($plugin, $name) = pluginSplit($task, true); - if (!isset($alias)) { - $alias = $name; - } + /** + * Loads/constructs a task. Will return the instance in the registry if it already exists. + * + * You can alias your task as an existing task by setting the 'className' key, i.e., + * ``` + * public $tasks = array( + * 'DbConfig' => array( + * 'className' => 'Bakeplus.DbConfigure' + * ); + * ); + * ``` + * All calls to the `DbConfig` task would use `DbConfigure` found in the `Bakeplus` plugin instead. + * + * @param string $task Task name to load + * @param array $settings Settings for the task. + * @return AppShell A task object, Either the existing loaded task or a new one. + * @throws MissingTaskException when the task could not be found + */ + public function load($task, $settings = []) + { + if (is_array($settings) && isset($settings['className'])) { + $alias = $task; + $task = $settings['className']; + } + list($plugin, $name) = pluginSplit($task, true); + if (!isset($alias)) { + $alias = $name; + } - if (isset($this->_loaded[$alias])) { - return $this->_loaded[$alias]; - } - $taskClass = $name . 'Task'; - App::uses($taskClass, $plugin . 'Console/Command/Task'); + if (isset($this->_loaded[$alias])) { + return $this->_loaded[$alias]; + } + $taskClass = $name . 'Task'; + App::uses($taskClass, $plugin . 'Console/Command/Task'); - $exists = class_exists($taskClass); - if (!$exists) { - throw new MissingTaskException(array( - 'class' => $taskClass, - 'plugin' => substr($plugin, 0, -1) - )); - } + $exists = class_exists($taskClass); + if (!$exists) { + throw new MissingTaskException([ + 'class' => $taskClass, + 'plugin' => substr($plugin, 0, -1) + ]); + } - $this->_loaded[$alias] = new $taskClass( - $this->_Shell->stdout, $this->_Shell->stderr, $this->_Shell->stdin - ); - return $this->_loaded[$alias]; - } + $this->_loaded[$alias] = new $taskClass( + $this->_Shell->stdout, $this->_Shell->stderr, $this->_Shell->stdin + ); + return $this->_loaded[$alias]; + } } diff --git a/lib/Cake/Console/Templates/default/actions/controller_actions.ctp b/lib/Cake/Console/Templates/default/actions/controller_actions.ctp index 0cde5c63..2bb6a7a1 100755 --- a/lib/Cake/Console/Templates/default/actions/controller_actions.ctp +++ b/lib/Cake/Console/Templates/default/actions/controller_actions.ctp @@ -18,134 +18,134 @@ ?> /** - * index method - * - * @return void - */ - public function index() { - $this->->recursive = 0; - $this->set('', $this->Paginator->paginate()); - } +* index method +* +* @return void +*/ +public function index() { +$this->->recursive = 0; +$this->set('', $this->Paginator->paginate()); +} /** - * view method - * - * @throws NotFoundException - * @param string $id - * @return void - */ - public function view($id = null) { - if (!$this->->exists($id)) { - throw new NotFoundException(__('Invalid ')); - } - $options = array('conditions' => array('.' . $this->->primaryKey => $id)); - $this->set('', $this->->find('first', $options)); - } +* view method +* +* @throws NotFoundException +* @param string $id +* @return void +*/ +public function view($id = null) { +if (!$this->->exists($id)) { +throw new NotFoundException(__('Invalid ')); +} +$options = array('conditions' => array('.' . $this->->primaryKey => $id)); +$this->set('', $this->->find('first', $options)); +} - + /** - * add method - * - * @return void - */ - public function add() { - if ($this->request->is('post')) { - $this->->create(); - if ($this->->save($this->request->data)) { +* add method +* +* @return void +*/ +public function add() { +if ($this->request->is('post')) { +$this->->create(); +if ($this->->save($this->request->data)) { - $this->Flash->success(__('The has been saved.')); - return $this->redirect(array('action' => 'index')); - } else { - $this->Flash->error(__('The could not be saved. Please, try again.')); + $this->Flash->success(__('The has been saved.')); + return $this->redirect(array('action' => 'index')); + } else { + $this->Flash->error(__('The could not be saved. Please, try again.')); - return $this->flash(__('The has been saved.'), array('action' => 'index')); + return $this->flash(__('The has been saved.'), array('action' => 'index')); - } - } +} +} {$assoc} as $associationName => $relation): - if (!empty($associationName)): - $otherModelName = $this->_modelName($associationName); - $otherPluralName = $this->_pluralName($associationName); - echo "\t\t\${$otherPluralName} = \$this->{$currentModelName}->{$otherModelName}->find('list');\n"; - $compact[] = "'{$otherPluralName}'"; - endif; - endforeach; - endforeach; - if (!empty($compact)): - echo "\t\t\$this->set(compact(".join(', ', $compact)."));\n"; - endif; +foreach (['belongsTo', 'hasAndBelongsToMany'] as $assoc): + foreach ($modelObj->{$assoc} as $associationName => $relation): + if (!empty($associationName)): + $otherModelName = $this->_modelName($associationName); + $otherPluralName = $this->_pluralName($associationName); + echo "\t\t\${$otherPluralName} = \$this->{$currentModelName}->{$otherModelName}->find('list');\n"; + $compact[] = "'{$otherPluralName}'"; + endif; + endforeach; +endforeach; +if (!empty($compact)): + echo "\t\t\$this->set(compact(" . join(', ', $compact) . "));\n"; +endif; ?> - } +} - + /** - * edit method - * - * @throws NotFoundException - * @param string $id - * @return void - */ - public function edit($id = null) { - if (!$this->->exists($id)) { - throw new NotFoundException(__('Invalid ')); - } - if ($this->request->is(array('post', 'put'))) { - if ($this->->save($this->request->data)) { +* edit method +* +* @throws NotFoundException +* @param string $id +* @return void +*/ +public function edit($id = null) { +if (!$this->->exists($id)) { +throw new NotFoundException(__('Invalid ')); +} +if ($this->request->is(array('post', 'put'))) { +if ($this->->save($this->request->data)) { - $this->Flash->success(__('The has been saved.')); - return $this->redirect(array('action' => 'index')); - } else { - $this->Flash->error(__('The could not be saved. Please, try again.')); + $this->Flash->success(__('The has been saved.')); + return $this->redirect(array('action' => 'index')); + } else { + $this->Flash->error(__('The could not be saved. Please, try again.')); - return $this->flash(__('The has been saved.'), array('action' => 'index')); + return $this->flash(__('The has been saved.'), array('action' => 'index')); - } - } else { - $options = array('conditions' => array('.' . $this->->primaryKey => $id)); - $this->request->data = $this->->find('first', $options); - } +} +} else { +$options = array('conditions' => array('.' . $this->->primaryKey => $id)); +$this->request->data = $this->->find('first', $options); +} {$assoc} as $associationName => $relation): - if (!empty($associationName)): - $otherModelName = $this->_modelName($associationName); - $otherPluralName = $this->_pluralName($associationName); - echo "\t\t\${$otherPluralName} = \$this->{$currentModelName}->{$otherModelName}->find('list');\n"; - $compact[] = "'{$otherPluralName}'"; - endif; - endforeach; - endforeach; - if (!empty($compact)): - echo "\t\t\$this->set(compact(".join(', ', $compact)."));\n"; - endif; - ?> - } +foreach (['belongsTo', 'hasAndBelongsToMany'] as $assoc): + foreach ($modelObj->{$assoc} as $associationName => $relation): + if (!empty($associationName)): + $otherModelName = $this->_modelName($associationName); + $otherPluralName = $this->_pluralName($associationName); + echo "\t\t\${$otherPluralName} = \$this->{$currentModelName}->{$otherModelName}->find('list');\n"; + $compact[] = "'{$otherPluralName}'"; + endif; + endforeach; +endforeach; +if (!empty($compact)): + echo "\t\t\$this->set(compact(" . join(', ', $compact) . "));\n"; +endif; +?> +} /** - * delete method - * - * @throws NotFoundException - * @param string $id - * @return void - */ - public function delete($id = null) { - if (!$this->->exists($id)) { - throw new NotFoundException(__('Invalid ')); - } - $this->request->allowMethod('post', 'delete'); - if ($this->->delete($id)) { +* delete method +* +* @throws NotFoundException +* @param string $id +* @return void +*/ +public function delete($id = null) { +if (!$this->->exists($id)) { +throw new NotFoundException(__('Invalid ')); +} +$this->request->allowMethod('post', 'delete'); +if ($this->->delete($id)) { - $this->Flash->success(__('The has been deleted.')); - } else { - $this->Flash->error(__('The could not be deleted. Please, try again.')); - } - return $this->redirect(array('action' => 'index')); + $this->Flash->success(__('The has been deleted.')); + } else { + $this->Flash->error(__('The could not be deleted. Please, try again.')); + } + return $this->redirect(array('action' => 'index')); - return $this->flash(__('The has been deleted.'), array('action' => 'index')); - } else { - return $this->flash(__('The could not be deleted. Please, try again.'), array('action' => 'index')); - } + return $this->flash(__('The has been deleted.'), array('action' => 'index')); + } else { + return $this->flash(__('The could not be deleted. Please, try again.'), array('action' => 'index')); + } - } +} diff --git a/lib/Cake/Console/Templates/default/classes/controller.ctp b/lib/Cake/Console/Templates/default/classes/controller.ctp index 30a9846d..10649e4c 100755 --- a/lib/Cake/Console/Templates/default/classes/controller.ctp +++ b/lib/Cake/Console/Templates/default/classes/controller.ctp @@ -22,61 +22,61 @@ echo " /** - * Controller +* Controller - */ +*/ class Controller extends AppController { -/** - * Scaffold - * - * @var mixed - */ - public $scaffold; + /** + * Scaffold + * + * @var mixed + */ + public $scaffold; } diff --git a/lib/Cake/Console/Templates/default/classes/fixture.ctp b/lib/Cake/Console/Templates/default/classes/fixture.ctp index 887843ad..5aa0f7cf 100755 --- a/lib/Cake/Console/Templates/default/classes/fixture.ctp +++ b/lib/Cake/Console/Templates/default/classes/fixture.ctp @@ -21,44 +21,44 @@ echo " /** - * Fixture - */ +* Fixture +*/ class Fixture extends CakeTestFixture { -/** - * Table name - * - * @var string - */ - public $table = ''; + /** + * Table name + * + * @var string + */ + public $table = ''; -/** - * Import - * - * @var array - */ - public $import = ; + /** + * Import + * + * @var array + */ + public $import = ; -/** - * Fields - * - * @var array - */ - public $fields = ; + /** + * Fields + * + * @var array + */ + public $fields = ; -/** - * Records - * - * @var array - */ - public $records = ; + /** + * Records + * + * @var array + */ + public $records = ; } diff --git a/lib/Cake/Console/Templates/default/classes/model.ctp b/lib/Cake/Console/Templates/default/classes/model.ctp index 8b795245..d75daa4b 100755 --- a/lib/Cake/Console/Templates/default/classes/model.ctp +++ b/lib/Cake/Console/Templates/default/classes/model.ctp @@ -22,168 +22,171 @@ echo " /** - * Model - * +* Model +* - */ +*/ class extends AppModel { -/** - * Use database config - * - * @var string - */ - public $useDbConfig = ''; + /** + * Use database config + * + * @var string + */ + public $useDbConfig = ''; -/** - * Primary key field - * - * @var string - */ - public $primaryKey = ''; + /** + * Primary key field + * + * @var string + */ + public $primaryKey = ''; -/** - * Display field - * - * @var string - */ - public $displayField = ''; + /** + * Display field + * + * @var string + */ + public $displayField = ''; -/** - * Behaviors - * - * @var array - */ - public $actsAs = array(); + /** + * Behaviors + * + * @var array + */ + public $actsAs = array(); $validations): - echo "\t\t'$field' => array(\n"; - foreach ($validations as $key => $validator): - echo "\t\t\t'$key' => array(\n"; - echo "\t\t\t\t'rule' => array('$validator'),\n"; - echo "\t\t\t\t//'message' => 'Your custom message here',\n"; - echo "\t\t\t\t//'allowEmpty' => false,\n"; - echo "\t\t\t\t//'required' => false,\n"; - echo "\t\t\t\t//'last' => false, // Stop validation after this rule\n"; - echo "\t\t\t\t//'on' => 'create', // Limit validation to 'create' or 'update' operations\n"; - echo "\t\t\t),\n"; - endforeach; - echo "\t\t),\n"; - endforeach; - echo "\t);\n"; + echo "/**\n * Validation rules\n *\n * @var array\n */\n"; + echo "\tpublic \$validate = array(\n"; + foreach ($validate as $field => $validations): + echo "\t\t'$field' => array(\n"; + foreach ($validations as $key => $validator): + echo "\t\t\t'$key' => array(\n"; + echo "\t\t\t\t'rule' => array('$validator'),\n"; + echo "\t\t\t\t//'message' => 'Your custom message here',\n"; + echo "\t\t\t\t//'allowEmpty' => false,\n"; + echo "\t\t\t\t//'required' => false,\n"; + echo "\t\t\t\t//'last' => false, // Stop validation after this rule\n"; + echo "\t\t\t\t//'on' => 'create', // Limit validation to 'create' or 'update' operations\n"; + echo "\t\t\t),\n"; + endforeach; + echo "\t\t),\n"; + endforeach; + echo "\t);\n"; endif; foreach ($associations as $assoc): - if (!empty($assoc)): -?> + if (!empty($assoc)): + ?> - // The Associations below have been created with all possible keys, those that are not needed can be removed - $relation): - $out = "\n\t\t'{$relation['alias']}' => array(\n"; - $out .= "\t\t\t'className' => '{$relation['className']}',\n"; - $out .= "\t\t\t'foreignKey' => '{$relation['foreignKey']}',\n"; - $out .= "\t\t\t'conditions' => '',\n"; - $out .= "\t\t\t'fields' => '',\n"; - $out .= "\t\t\t'order' => ''\n"; - $out .= "\t\t)"; - if ($i + 1 < $typeCount) { - $out .= ","; - } - echo $out; - endforeach; - echo "\n\t);\n"; - endif; +foreach (['hasOne', 'belongsTo'] as $assocType): + if (!empty($associations[$assocType])): + $typeCount = count($associations[$assocType]); + echo "\n/**\n * $assocType associations\n *\n * @var array\n */"; + echo "\n\tpublic \$$assocType = array("; + foreach ($associations[$assocType] as $i => $relation): + $out = "\n\t\t'{$relation['alias']}' => array(\n"; + $out .= "\t\t\t'className' => '{$relation['className']}',\n"; + $out .= "\t\t\t'foreignKey' => '{$relation['foreignKey']}',\n"; + $out .= "\t\t\t'conditions' => '',\n"; + $out .= "\t\t\t'fields' => '',\n"; + $out .= "\t\t\t'order' => ''\n"; + $out .= "\t\t)"; + if ($i + 1 < $typeCount) { + $out .= ","; + } + echo $out; + endforeach; + echo "\n\t);\n"; + endif; endforeach; if (!empty($associations['hasMany'])): - $belongsToCount = count($associations['hasMany']); - echo "\n/**\n * hasMany associations\n *\n * @var array\n */"; - echo "\n\tpublic \$hasMany = array("; - foreach ($associations['hasMany'] as $i => $relation): - $out = "\n\t\t'{$relation['alias']}' => array(\n"; - $out .= "\t\t\t'className' => '{$relation['className']}',\n"; - $out .= "\t\t\t'foreignKey' => '{$relation['foreignKey']}',\n"; - $out .= "\t\t\t'dependent' => false,\n"; - $out .= "\t\t\t'conditions' => '',\n"; - $out .= "\t\t\t'fields' => '',\n"; - $out .= "\t\t\t'order' => '',\n"; - $out .= "\t\t\t'limit' => '',\n"; - $out .= "\t\t\t'offset' => '',\n"; - $out .= "\t\t\t'exclusive' => '',\n"; - $out .= "\t\t\t'finderQuery' => '',\n"; - $out .= "\t\t\t'counterQuery' => ''\n"; - $out .= "\t\t)"; - if ($i + 1 < $belongsToCount) { - $out .= ","; - } - echo $out; - endforeach; - echo "\n\t);\n\n"; + $belongsToCount = count($associations['hasMany']); + echo "\n/**\n * hasMany associations\n *\n * @var array\n */"; + echo "\n\tpublic \$hasMany = array("; + foreach ($associations['hasMany'] as $i => $relation): + $out = "\n\t\t'{$relation['alias']}' => array(\n"; + $out .= "\t\t\t'className' => '{$relation['className']}',\n"; + $out .= "\t\t\t'foreignKey' => '{$relation['foreignKey']}',\n"; + $out .= "\t\t\t'dependent' => false,\n"; + $out .= "\t\t\t'conditions' => '',\n"; + $out .= "\t\t\t'fields' => '',\n"; + $out .= "\t\t\t'order' => '',\n"; + $out .= "\t\t\t'limit' => '',\n"; + $out .= "\t\t\t'offset' => '',\n"; + $out .= "\t\t\t'exclusive' => '',\n"; + $out .= "\t\t\t'finderQuery' => '',\n"; + $out .= "\t\t\t'counterQuery' => ''\n"; + $out .= "\t\t)"; + if ($i + 1 < $belongsToCount) { + $out .= ","; + } + echo $out; + endforeach; + echo "\n\t);\n\n"; endif; if (!empty($associations['hasAndBelongsToMany'])): - $habtmCount = count($associations['hasAndBelongsToMany']); - echo "\n/**\n * hasAndBelongsToMany associations\n *\n * @var array\n */"; - echo "\n\tpublic \$hasAndBelongsToMany = array("; - foreach ($associations['hasAndBelongsToMany'] as $i => $relation): - $out = "\n\t\t'{$relation['alias']}' => array(\n"; - $out .= "\t\t\t'className' => '{$relation['className']}',\n"; - $out .= "\t\t\t'joinTable' => '{$relation['joinTable']}',\n"; - $out .= "\t\t\t'foreignKey' => '{$relation['foreignKey']}',\n"; - $out .= "\t\t\t'associationForeignKey' => '{$relation['associationForeignKey']}',\n"; - $out .= "\t\t\t'unique' => 'keepExisting',\n"; - $out .= "\t\t\t'conditions' => '',\n"; - $out .= "\t\t\t'fields' => '',\n"; - $out .= "\t\t\t'order' => '',\n"; - $out .= "\t\t\t'limit' => '',\n"; - $out .= "\t\t\t'offset' => '',\n"; - $out .= "\t\t\t'finderQuery' => '',\n"; - $out .= "\t\t)"; - if ($i + 1 < $habtmCount) { - $out .= ","; - } - echo $out; - endforeach; - echo "\n\t);\n\n"; + $habtmCount = count($associations['hasAndBelongsToMany']); + echo "\n/**\n * hasAndBelongsToMany associations\n *\n * @var array\n */"; + echo "\n\tpublic \$hasAndBelongsToMany = array("; + foreach ($associations['hasAndBelongsToMany'] as $i => $relation): + $out = "\n\t\t'{$relation['alias']}' => array(\n"; + $out .= "\t\t\t'className' => '{$relation['className']}',\n"; + $out .= "\t\t\t'joinTable' => '{$relation['joinTable']}',\n"; + $out .= "\t\t\t'foreignKey' => '{$relation['foreignKey']}',\n"; + $out .= "\t\t\t'associationForeignKey' => '{$relation['associationForeignKey']}',\n"; + $out .= "\t\t\t'unique' => 'keepExisting',\n"; + $out .= "\t\t\t'conditions' => '',\n"; + $out .= "\t\t\t'fields' => '',\n"; + $out .= "\t\t\t'order' => '',\n"; + $out .= "\t\t\t'limit' => '',\n"; + $out .= "\t\t\t'offset' => '',\n"; + $out .= "\t\t\t'finderQuery' => '',\n"; + $out .= "\t\t)"; + if ($i + 1 < $habtmCount) { + $out .= ","; + } + echo $out; + endforeach; + echo "\n\t);\n\n"; endif; ?> } diff --git a/lib/Cake/Console/Templates/default/classes/test.ctp b/lib/Cake/Console/Templates/default/classes/test.ctp index cf126491..ed87c745 100755 --- a/lib/Cake/Console/Templates/default/classes/test.ctp +++ b/lib/Cake/Console/Templates/default/classes/test.ctp @@ -19,63 +19,63 @@ echo " -App::uses('', ''); + App::uses('', ''); /** - * Test Case - */ +* Test Case +*/ -class Test extends ControllerTestCase { + class Test extends ControllerTestCase { -class Test extends CakeTestCase { + class Test extends CakeTestCase { -/** - * Fixtures - * - * @var array - */ - public $fixtures = array( - '' - ); + /** + * Fixtures + * + * @var array + */ + public $fixtures = array( + '' + ); -/** - * setUp method - * - * @return void - */ - public function setUp() { - parent::setUp(); - - $this-> - - } + /** + * setUp method + * + * @return void + */ + public function setUp() { + parent::setUp(); + + $this-> + + } -/** - * tearDown method - * - * @return void - */ - public function tearDown() { - unset($this->); + /** + * tearDown method + * + * @return void + */ + public function tearDown() { + unset($this->); - parent::tearDown(); - } + parent::tearDown(); + } -/** - * test method - * - * @return void - */ - public function test() { - $this->markTestIncomplete('test not implemented.'); - } + /** + * test method + * + * @return void + */ + public function test() { + $this->markTestIncomplete('test not implemented.'); + } } diff --git a/lib/Cake/Console/Templates/default/views/form.ctp b/lib/Cake/Console/Templates/default/views/form.ctp index 81b85dbc..a2866e9d 100755 --- a/lib/Cake/Console/Templates/default/views/form.ctp +++ b/lib/Cake/Console/Templates/default/views/form.ctp @@ -15,49 +15,49 @@ */ ?>
-Form->create('{$modelClass}'); ?>\n"; ?> -
- ", Inflector::humanize($action), $singularHumanName); ?> -Form->input('{$field}');\n"; - } - } - if (!empty($associations['hasAndBelongsToMany'])) { - foreach ($associations['hasAndBelongsToMany'] as $assocName => $assocData) { - echo "\t\techo \$this->Form->input('{$assocName}');\n"; - } - } - echo "\t?>\n"; -?> -
-Form->end(__('Submit')); ?>\n"; -?> + Form->create('{$modelClass}'); ?>\n"; ?> +
+ ", Inflector::humanize($action), $singularHumanName); ?> + Form->input('{$field}');\n"; + } + } + if (!empty($associations['hasAndBelongsToMany'])) { + foreach ($associations['hasAndBelongsToMany'] as $assocName => $assocData) { + echo "\t\techo \$this->Form->input('{$assocName}');\n"; + } + } + echo "\t?>\n"; + ?> +
+ Form->end(__('Submit')); ?>\n"; + ?>
-

"; ?>

-
    +

    "; ?>

    +
      - -
    • Form->postLink(__('Delete'), array('action' => 'delete', \$this->Form->value('{$modelClass}.{$primaryKey}')), array('confirm' => __('Are you sure you want to delete # %s?', \$this->Form->value('{$modelClass}.{$primaryKey}')))); ?>"; ?>
    • - -
    • Html->link(__('List " . $pluralHumanName . "'), array('action' => 'index')); ?>"; ?>
    • - $data) { - foreach ($data as $alias => $details) { - if ($details['controller'] != $this->name && !in_array($details['controller'], $done)) { - echo "\t\t
    • Html->link(__('List " . Inflector::humanize($details['controller']) . "'), array('controller' => '{$details['controller']}', 'action' => 'index')); ?>
    • \n"; - echo "\t\t
    • Html->link(__('New " . Inflector::humanize(Inflector::underscore($alias)) . "'), array('controller' => '{$details['controller']}', 'action' => 'add')); ?>
    • \n"; - $done[] = $details['controller']; - } - } - } -?> -
    + +
  • Form->postLink(__('Delete'), array('action' => 'delete', \$this->Form->value('{$modelClass}.{$primaryKey}')), array('confirm' => __('Are you sure you want to delete # %s?', \$this->Form->value('{$modelClass}.{$primaryKey}')))); ?>"; ?>
  • + +
  • Html->link(__('List " . $pluralHumanName . "'), array('action' => 'index')); ?>"; ?>
  • + $data) { + foreach ($data as $alias => $details) { + if ($details['controller'] != $this->name && !in_array($details['controller'], $done)) { + echo "\t\t
  • Html->link(__('List " . Inflector::humanize($details['controller']) . "'), array('controller' => '{$details['controller']}', 'action' => 'index')); ?>
  • \n"; + echo "\t\t
  • Html->link(__('New " . Inflector::humanize(Inflector::underscore($alias)) . "'), array('controller' => '{$details['controller']}', 'action' => 'add')); ?>
  • \n"; + $done[] = $details['controller']; + } + } + } + ?> +
diff --git a/lib/Cake/Console/Templates/default/views/index.ctp b/lib/Cake/Console/Templates/default/views/index.ctp index 72277141..26683b06 100755 --- a/lib/Cake/Console/Templates/default/views/index.ctp +++ b/lib/Cake/Console/Templates/default/views/index.ctp @@ -15,79 +15,79 @@ */ ?>
-

"; ?>

- - - - - - - - - - - \n"; - echo "\t\n"; - foreach ($fields as $field) { - $isKey = false; - if (!empty($associations['belongsTo'])) { - foreach ($associations['belongsTo'] as $alias => $details) { - if ($field === $details['foreignKey']) { - $isKey = true; - echo "\t\t\n"; - break; - } - } - } - if ($isKey !== true) { - echo "\t\t\n"; - } - } +

"; ?>

+
Paginator->sort('{$field}'); ?>"; ?>"; ?>
\n\t\t\tHtml->link(\${$singularVar}['{$alias}']['{$details['displayField']}'], array('controller' => '{$details['controller']}', 'action' => 'view', \${$singularVar}['{$alias}']['{$details['primaryKey']}'])); ?>\n\t\t 
+ + + + + + + + + + \n"; + echo "\t\n"; + foreach ($fields as $field) { + $isKey = false; + if (!empty($associations['belongsTo'])) { + foreach ($associations['belongsTo'] as $alias => $details) { + if ($field === $details['foreignKey']) { + $isKey = true; + echo "\t\t\n"; + break; + } + } + } + if ($isKey !== true) { + echo "\t\t\n"; + } + } - echo "\t\t\n"; - echo "\t\n"; + echo "\t\t\n"; + echo "\t\n"; - echo "\n"; - ?> - -
Paginator->sort('{$field}'); ?>"; ?>"; ?>
\n\t\t\tHtml->link(\${$singularVar}['{$alias}']['{$details['displayField']}'], array('controller' => '{$details['controller']}', 'action' => 'view', \${$singularVar}['{$alias}']['{$details['primaryKey']}'])); ?>\n\t\t \n"; - echo "\t\t\tHtml->link(__('View'), array('action' => 'view', \${$singularVar}['{$modelClass}']['{$primaryKey}'])); ?>\n"; - echo "\t\t\tHtml->link(__('Edit'), array('action' => 'edit', \${$singularVar}['{$modelClass}']['{$primaryKey}'])); ?>\n"; - echo "\t\t\tForm->postLink(__('Delete'), array('action' => 'delete', \${$singularVar}['{$modelClass}']['{$primaryKey}']), array('confirm' => __('Are you sure you want to delete # %s?', \${$singularVar}['{$modelClass}']['{$primaryKey}']))); ?>\n"; - echo "\t\t
\n"; + echo "\t\t\tHtml->link(__('View'), array('action' => 'view', \${$singularVar}['{$modelClass}']['{$primaryKey}'])); ?>\n"; + echo "\t\t\tHtml->link(__('Edit'), array('action' => 'edit', \${$singularVar}['{$modelClass}']['{$primaryKey}'])); ?>\n"; + echo "\t\t\tForm->postLink(__('Delete'), array('action' => 'delete', \${$singularVar}['{$modelClass}']['{$primaryKey}']), array('confirm' => __('Are you sure you want to delete # %s?', \${$singularVar}['{$modelClass}']['{$primaryKey}']))); ?>\n"; + echo "\t\t
-

- \n"; + ?> + + +

+ Paginator->counter(array( 'format' => __('Page {:page} of {:pages}, showing {:current} records out of {:count} total, starting on record {:start}, ending on {:end}') )); ?>"; ?> -

-
- Paginator->prev('< ' . __('previous'), array(), null, array('class' => 'prev disabled'));\n"; - echo "\t\techo \$this->Paginator->numbers(array('separator' => ''));\n"; - echo "\t\techo \$this->Paginator->next(__('next') . ' >', array(), null, array('class' => 'next disabled'));\n"; - echo "\t?>\n"; - ?> -
+

+
+ Paginator->prev('< ' . __('previous'), array(), null, array('class' => 'prev disabled'));\n"; + echo "\t\techo \$this->Paginator->numbers(array('separator' => ''));\n"; + echo "\t\techo \$this->Paginator->next(__('next') . ' >', array(), null, array('class' => 'next disabled'));\n"; + echo "\t?>\n"; + ?> +
-

"; ?>

-
    -
  • Html->link(__('New " . $singularHumanName . "'), array('action' => 'add')); ?>"; ?>
  • - $data) { - foreach ($data as $alias => $details) { - if ($details['controller'] != $this->name && !in_array($details['controller'], $done)) { - echo "\t\t
  • Html->link(__('List " . Inflector::humanize($details['controller']) . "'), array('controller' => '{$details['controller']}', 'action' => 'index')); ?>
  • \n"; - echo "\t\t
  • Html->link(__('New " . Inflector::humanize(Inflector::underscore($alias)) . "'), array('controller' => '{$details['controller']}', 'action' => 'add')); ?>
  • \n"; - $done[] = $details['controller']; - } - } - } -?> -
+

"; ?>

+
    +
  • Html->link(__('New " . $singularHumanName . "'), array('action' => 'add')); ?>"; ?>
  • + $data) { + foreach ($data as $alias => $details) { + if ($details['controller'] != $this->name && !in_array($details['controller'], $done)) { + echo "\t\t
  • Html->link(__('List " . Inflector::humanize($details['controller']) . "'), array('controller' => '{$details['controller']}', 'action' => 'index')); ?>
  • \n"; + echo "\t\t
  • Html->link(__('New " . Inflector::humanize(Inflector::underscore($alias)) . "'), array('controller' => '{$details['controller']}', 'action' => 'add')); ?>
  • \n"; + $done[] = $details['controller']; + } + } + } + ?> +
diff --git a/lib/Cake/Console/Templates/default/views/view.ctp b/lib/Cake/Console/Templates/default/views/view.ctp index b229b9f1..9abc4bc3 100755 --- a/lib/Cake/Console/Templates/default/views/view.ctp +++ b/lib/Cake/Console/Templates/default/views/view.ctp @@ -15,122 +15,122 @@ */ ?>
-

"; ?>

-
- $details) { - if ($field === $details['foreignKey']) { - $isKey = true; - echo "\t\t
\n"; - echo "\t\t
\n\t\t\tHtml->link(\${$singularVar}['{$alias}']['{$details['displayField']}'], array('controller' => '{$details['controller']}', 'action' => 'view', \${$singularVar}['{$alias}']['{$details['primaryKey']}'])); ?>\n\t\t\t \n\t\t
\n"; - break; - } - } - } - if ($isKey !== true) { - echo "\t\t
\n"; - echo "\t\t
\n\t\t\t\n\t\t\t \n\t\t
\n"; - } -} -?> -
+

"; ?>

+
+ $details) { + if ($field === $details['foreignKey']) { + $isKey = true; + echo "\t\t
\n"; + echo "\t\t
\n\t\t\tHtml->link(\${$singularVar}['{$alias}']['{$details['displayField']}'], array('controller' => '{$details['controller']}', 'action' => 'view', \${$singularVar}['{$alias}']['{$details['primaryKey']}'])); ?>\n\t\t\t \n\t\t
\n"; + break; + } + } + } + if ($isKey !== true) { + echo "\t\t
\n"; + echo "\t\t
\n\t\t\t\n\t\t\t \n\t\t
\n"; + } + } + ?> +
-

"; ?>

-
    -Html->link(__('Edit " . $singularHumanName ."'), array('action' => 'edit', \${$singularVar}['{$modelClass}']['{$primaryKey}'])); ?> \n"; - echo "\t\t
  • Form->postLink(__('Delete " . $singularHumanName . "'), array('action' => 'delete', \${$singularVar}['{$modelClass}']['{$primaryKey}']), array('confirm' => __('Are you sure you want to delete # %s?', \${$singularVar}['{$modelClass}']['{$primaryKey}']))); ?>
  • \n"; - echo "\t\t
  • Html->link(__('List " . $pluralHumanName . "'), array('action' => 'index')); ?>
  • \n"; - echo "\t\t
  • Html->link(__('New " . $singularHumanName . "'), array('action' => 'add')); ?>
  • \n"; +

    "; ?>

    +
      + Html->link(__('Edit " . $singularHumanName . "'), array('action' => 'edit', \${$singularVar}['{$modelClass}']['{$primaryKey}'])); ?> \n"; + echo "\t\t
    • Form->postLink(__('Delete " . $singularHumanName . "'), array('action' => 'delete', \${$singularVar}['{$modelClass}']['{$primaryKey}']), array('confirm' => __('Are you sure you want to delete # %s?', \${$singularVar}['{$modelClass}']['{$primaryKey}']))); ?>
    • \n"; + echo "\t\t
    • Html->link(__('List " . $pluralHumanName . "'), array('action' => 'index')); ?>
    • \n"; + echo "\t\t
    • Html->link(__('New " . $singularHumanName . "'), array('action' => 'add')); ?>
    • \n"; - $done = array(); - foreach ($associations as $type => $data) { - foreach ($data as $alias => $details) { - if ($details['controller'] != $this->name && !in_array($details['controller'], $done)) { - echo "\t\t
    • Html->link(__('List " . Inflector::humanize($details['controller']) . "'), array('controller' => '{$details['controller']}', 'action' => 'index')); ?>
    • \n"; - echo "\t\t
    • Html->link(__('New " . Inflector::humanize(Inflector::underscore($alias)) . "'), array('controller' => '{$details['controller']}', 'action' => 'add')); ?>
    • \n"; - $done[] = $details['controller']; - } - } - } -?> -
    + $done = []; + foreach ($associations as $type => $data) { + foreach ($data as $alias => $details) { + if ($details['controller'] != $this->name && !in_array($details['controller'], $done)) { + echo "\t\t
  • Html->link(__('List " . Inflector::humanize($details['controller']) . "'), array('controller' => '{$details['controller']}', 'action' => 'index')); ?>
  • \n"; + echo "\t\t
  • Html->link(__('New " . Inflector::humanize(Inflector::underscore($alias)) . "'), array('controller' => '{$details['controller']}', 'action' => 'add')); ?>
  • \n"; + $done[] = $details['controller']; + } + } + } + ?> +
$details): ?> - - $details): ?> + + $details): - $otherSingularVar = Inflector::variable($alias); - $otherPluralHumanName = Inflector::humanize($details['controller']); - ?> - + echo "\t\n"; + ?> + + \n\n"; ?> +
+
    +
  • Html->link(__('New " . Inflector::humanize(Inflector::underscore($alias)) . "'), array('controller' => '{$details['controller']}', 'action' => 'add')); ?>"; ?>
  • +
+
+ diff --git a/lib/Cake/Console/Templates/skel/Config/Schema/db_acl.php b/lib/Cake/Console/Templates/skel/Config/Schema/db_acl.php index 12ad8ee9..ac5b7530 100755 --- a/lib/Cake/Console/Templates/skel/Config/Schema/db_acl.php +++ b/lib/Cake/Console/Templates/skel/Config/Schema/db_acl.php @@ -13,68 +13,69 @@ * Using the Schema command line utility * cake schema run create DbAcl */ -class DbAclSchema extends CakeSchema { +class DbAclSchema extends CakeSchema +{ -/** - * Before event. - * - * @param array $event The event data. - * @return bool success - */ - public function before($event = array()) { - return true; - } - -/** - * After event. - * - * @param array $event The event data. - * @return void - */ - public function after($event = array()) { - } + /** + * ACO - Access Control Object - Something that is wanted + */ + public $acos = [ + 'id' => ['type' => 'integer', 'null' => false, 'default' => null, 'length' => 10, 'key' => 'primary'], + 'parent_id' => ['type' => 'integer', 'null' => true, 'default' => null, 'length' => 10], + 'model' => ['type' => 'string', 'null' => true], + 'foreign_key' => ['type' => 'integer', 'null' => true, 'default' => null, 'length' => 10], + 'alias' => ['type' => 'string', 'null' => true], + 'lft' => ['type' => 'integer', 'null' => true, 'default' => null, 'length' => 10], + 'rght' => ['type' => 'integer', 'null' => true, 'default' => null, 'length' => 10], + 'indexes' => ['PRIMARY' => ['column' => 'id', 'unique' => 1]] + ]; + /** + * ARO - Access Request Object - Something that wants something + */ + public $aros = [ + 'id' => ['type' => 'integer', 'null' => false, 'default' => null, 'length' => 10, 'key' => 'primary'], + 'parent_id' => ['type' => 'integer', 'null' => true, 'default' => null, 'length' => 10], + 'model' => ['type' => 'string', 'null' => true], + 'foreign_key' => ['type' => 'integer', 'null' => true, 'default' => null, 'length' => 10], + 'alias' => ['type' => 'string', 'null' => true], + 'lft' => ['type' => 'integer', 'null' => true, 'default' => null, 'length' => 10], + 'rght' => ['type' => 'integer', 'null' => true, 'default' => null, 'length' => 10], + 'indexes' => ['PRIMARY' => ['column' => 'id', 'unique' => 1]] + ]; + /** + * Used by the Cake::Model:Permission class. + * Checks if the given $aro has access to action $action in $aco. + */ + public $aros_acos = [ + 'id' => ['type' => 'integer', 'null' => false, 'default' => null, 'length' => 10, 'key' => 'primary'], + 'aro_id' => ['type' => 'integer', 'null' => false, 'length' => 10, 'key' => 'index'], + 'aco_id' => ['type' => 'integer', 'null' => false, 'length' => 10], + '_create' => ['type' => 'string', 'null' => false, 'default' => '0', 'length' => 2], + '_read' => ['type' => 'string', 'null' => false, 'default' => '0', 'length' => 2], + '_update' => ['type' => 'string', 'null' => false, 'default' => '0', 'length' => 2], + '_delete' => ['type' => 'string', 'null' => false, 'default' => '0', 'length' => 2], + 'indexes' => ['PRIMARY' => ['column' => 'id', 'unique' => 1], 'ARO_ACO_KEY' => ['column' => ['aro_id', 'aco_id'], 'unique' => 1]] + ]; -/** - * ACO - Access Control Object - Something that is wanted - */ - public $acos = array( - 'id' => array('type' => 'integer', 'null' => false, 'default' => null, 'length' => 10, 'key' => 'primary'), - 'parent_id' => array('type' => 'integer', 'null' => true, 'default' => null, 'length' => 10), - 'model' => array('type' => 'string', 'null' => true), - 'foreign_key' => array('type' => 'integer', 'null' => true, 'default' => null, 'length' => 10), - 'alias' => array('type' => 'string', 'null' => true), - 'lft' => array('type' => 'integer', 'null' => true, 'default' => null, 'length' => 10), - 'rght' => array('type' => 'integer', 'null' => true, 'default' => null, 'length' => 10), - 'indexes' => array('PRIMARY' => array('column' => 'id', 'unique' => 1)) - ); + /** + * Before event. + * + * @param array $event The event data. + * @return bool success + */ + public function before($event = []) + { + return true; + } -/** - * ARO - Access Request Object - Something that wants something - */ - public $aros = array( - 'id' => array('type' => 'integer', 'null' => false, 'default' => null, 'length' => 10, 'key' => 'primary'), - 'parent_id' => array('type' => 'integer', 'null' => true, 'default' => null, 'length' => 10), - 'model' => array('type' => 'string', 'null' => true), - 'foreign_key' => array('type' => 'integer', 'null' => true, 'default' => null, 'length' => 10), - 'alias' => array('type' => 'string', 'null' => true), - 'lft' => array('type' => 'integer', 'null' => true, 'default' => null, 'length' => 10), - 'rght' => array('type' => 'integer', 'null' => true, 'default' => null, 'length' => 10), - 'indexes' => array('PRIMARY' => array('column' => 'id', 'unique' => 1)) - ); - -/** - * Used by the Cake::Model:Permission class. - * Checks if the given $aro has access to action $action in $aco. - */ - public $aros_acos = array( - 'id' => array('type' => 'integer', 'null' => false, 'default' => null, 'length' => 10, 'key' => 'primary'), - 'aro_id' => array('type' => 'integer', 'null' => false, 'length' => 10, 'key' => 'index'), - 'aco_id' => array('type' => 'integer', 'null' => false, 'length' => 10), - '_create' => array('type' => 'string', 'null' => false, 'default' => '0', 'length' => 2), - '_read' => array('type' => 'string', 'null' => false, 'default' => '0', 'length' => 2), - '_update' => array('type' => 'string', 'null' => false, 'default' => '0', 'length' => 2), - '_delete' => array('type' => 'string', 'null' => false, 'default' => '0', 'length' => 2), - 'indexes' => array('PRIMARY' => array('column' => 'id', 'unique' => 1), 'ARO_ACO_KEY' => array('column' => array('aro_id', 'aco_id'), 'unique' => 1)) - ); + /** + * After event. + * + * @param array $event The event data. + * @return void + */ + public function after($event = []) + { + } } diff --git a/lib/Cake/Console/Templates/skel/Config/Schema/db_acl.sql b/lib/Cake/Console/Templates/skel/Config/Schema/db_acl.sql index cbb0ccec..f9fd36f6 100755 --- a/lib/Cake/Console/Templates/skel/Config/Schema/db_acl.sql +++ b/lib/Cake/Console/Templates/skel/Config/Schema/db_acl.sql @@ -1,4 +1,5 @@ -# $Id$ +# +$Id$ # # Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org) # @@ -7,37 +8,40 @@ # Redistributions of files must retain the above copyright notice. # MIT License (https://opensource.org/licenses/mit-license.php) -CREATE TABLE acos ( - id INTEGER(10) UNSIGNED NOT NULL AUTO_INCREMENT, - parent_id INTEGER(10) DEFAULT NULL, - model VARCHAR(255) DEFAULT '', - foreign_key INTEGER(10) UNSIGNED DEFAULT NULL, - alias VARCHAR(255) DEFAULT '', - lft INTEGER(10) DEFAULT NULL, - rght INTEGER(10) DEFAULT NULL, - PRIMARY KEY (id) +CREATE TABLE acos +( + id INTEGER(10) UNSIGNED NOT NULL AUTO_INCREMENT, + parent_id INTEGER(10) DEFAULT NULL, + model VARCHAR(255) DEFAULT '', + foreign_key INTEGER(10) UNSIGNED DEFAULT NULL, + alias VARCHAR(255) DEFAULT '', + lft INTEGER(10) DEFAULT NULL, + rght INTEGER(10) DEFAULT NULL, + PRIMARY KEY (id) ); -CREATE TABLE aros_acos ( - id INTEGER(10) UNSIGNED NOT NULL AUTO_INCREMENT, - aro_id INTEGER(10) UNSIGNED NOT NULL, - aco_id INTEGER(10) UNSIGNED NOT NULL, - _create CHAR(2) NOT NULL DEFAULT 0, - _read CHAR(2) NOT NULL DEFAULT 0, - _update CHAR(2) NOT NULL DEFAULT 0, - _delete CHAR(2) NOT NULL DEFAULT 0, - PRIMARY KEY(id) +CREATE TABLE aros_acos +( + id INTEGER(10) UNSIGNED NOT NULL AUTO_INCREMENT, + aro_id INTEGER(10) UNSIGNED NOT NULL, + aco_id INTEGER(10) UNSIGNED NOT NULL, + _create CHAR(2) NOT NULL DEFAULT 0, + _read CHAR(2) NOT NULL DEFAULT 0, + _update CHAR(2) NOT NULL DEFAULT 0, + _delete CHAR(2) NOT NULL DEFAULT 0, + PRIMARY KEY (id) ); -CREATE TABLE aros ( - id INTEGER(10) UNSIGNED NOT NULL AUTO_INCREMENT, - parent_id INTEGER(10) DEFAULT NULL, - model VARCHAR(255) DEFAULT '', - foreign_key INTEGER(10) UNSIGNED DEFAULT NULL, - alias VARCHAR(255) DEFAULT '', - lft INTEGER(10) DEFAULT NULL, - rght INTEGER(10) DEFAULT NULL, - PRIMARY KEY (id) +CREATE TABLE aros +( + id INTEGER(10) UNSIGNED NOT NULL AUTO_INCREMENT, + parent_id INTEGER(10) DEFAULT NULL, + model VARCHAR(255) DEFAULT '', + foreign_key INTEGER(10) UNSIGNED DEFAULT NULL, + alias VARCHAR(255) DEFAULT '', + lft INTEGER(10) DEFAULT NULL, + rght INTEGER(10) DEFAULT NULL, + PRIMARY KEY (id) ); /* this indexes will improve acl perfomance */ diff --git a/lib/Cake/Console/Templates/skel/Config/Schema/i18n.php b/lib/Cake/Console/Templates/skel/Config/Schema/i18n.php index 63dc0db9..7c65418c 100755 --- a/lib/Cake/Console/Templates/skel/Config/Schema/i18n.php +++ b/lib/Cake/Console/Templates/skel/Config/Schema/i18n.php @@ -25,47 +25,49 @@ * * cake schema run create i18n */ -class I18nSchema extends CakeSchema { +class I18nSchema extends CakeSchema +{ -/** - * The name property - * - * @var string - */ - public $name = 'i18n'; + /** + * The name property + * + * @var string + */ + public $name = 'i18n'; + /** + * The i18n table definition + * + * @var array + */ + public $i18n = [ + 'id' => ['type' => 'integer', 'null' => false, 'default' => null, 'length' => 10, 'key' => 'primary'], + 'locale' => ['type' => 'string', 'null' => false, 'length' => 6, 'key' => 'index'], + 'model' => ['type' => 'string', 'null' => false, 'key' => 'index'], + 'foreign_key' => ['type' => 'integer', 'null' => false, 'length' => 10, 'key' => 'index'], + 'field' => ['type' => 'string', 'null' => false, 'key' => 'index'], + 'content' => ['type' => 'text', 'null' => true, 'default' => null], + 'indexes' => ['PRIMARY' => ['column' => 'id', 'unique' => 1], 'locale' => ['column' => 'locale', 'unique' => 0], 'model' => ['column' => 'model', 'unique' => 0], 'row_id' => ['column' => 'foreign_key', 'unique' => 0], 'field' => ['column' => 'field', 'unique' => 0]] + ]; -/** - * Before callback. - * - * @param array $event Schema object properties - * @return bool Should process continue - */ - public function before($event = array()) { - return true; - } + /** + * Before callback. + * + * @param array $event Schema object properties + * @return bool Should process continue + */ + public function before($event = []) + { + return true; + } -/** - * After callback. - * - * @param array $event Schema object properties - * @return void - */ - public function after($event = array()) { - } - -/** - * The i18n table definition - * - * @var array - */ - public $i18n = array( - 'id' => array('type' => 'integer', 'null' => false, 'default' => null, 'length' => 10, 'key' => 'primary'), - 'locale' => array('type' => 'string', 'null' => false, 'length' => 6, 'key' => 'index'), - 'model' => array('type' => 'string', 'null' => false, 'key' => 'index'), - 'foreign_key' => array('type' => 'integer', 'null' => false, 'length' => 10, 'key' => 'index'), - 'field' => array('type' => 'string', 'null' => false, 'key' => 'index'), - 'content' => array('type' => 'text', 'null' => true, 'default' => null), - 'indexes' => array('PRIMARY' => array('column' => 'id', 'unique' => 1), 'locale' => array('column' => 'locale', 'unique' => 0), 'model' => array('column' => 'model', 'unique' => 0), 'row_id' => array('column' => 'foreign_key', 'unique' => 0), 'field' => array('column' => 'field', 'unique' => 0)) - ); + /** + * After callback. + * + * @param array $event Schema object properties + * @return void + */ + public function after($event = []) + { + } } diff --git a/lib/Cake/Console/Templates/skel/Config/Schema/i18n.sql b/lib/Cake/Console/Templates/skel/Config/Schema/i18n.sql index a1a4e689..640128d4 100755 --- a/lib/Cake/Console/Templates/skel/Config/Schema/i18n.sql +++ b/lib/Cake/Console/Templates/skel/Config/Schema/i18n.sql @@ -1,4 +1,5 @@ -# $Id$ +# +$Id$ # # Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org) # @@ -7,21 +8,22 @@ # Redistributions of files must retain the above copyright notice. # MIT License (https://opensource.org/licenses/mit-license.php) -CREATE TABLE i18n ( - id int(10) NOT NULL auto_increment, - locale varchar(6) NOT NULL, - model varchar(255) NOT NULL, - foreign_key int(10) NOT NULL, - field varchar(255) NOT NULL, - content mediumtext, - PRIMARY KEY (id), -# UNIQUE INDEX I18N_LOCALE_FIELD(locale, model, foreign_key, field), -# INDEX I18N_LOCALE_ROW(locale, model, foreign_key), -# INDEX I18N_LOCALE_MODEL(locale, model), -# INDEX I18N_FIELD(model, foreign_key, field), -# INDEX I18N_ROW(model, foreign_key), - INDEX locale (locale), - INDEX model (model), - INDEX row_id (foreign_key), - INDEX field (field) +CREATE TABLE i18n +( + id int(10) NOT NULL auto_increment, + locale varchar(6) NOT NULL, + model varchar(255) NOT NULL, + foreign_key int(10) NOT NULL, + field varchar(255) NOT NULL, + content mediumtext, + PRIMARY KEY (id), + # UNIQUE INDEX I18N_LOCALE_FIELD(locale, model, foreign_key, field), + # INDEX I18N_LOCALE_ROW(locale, model, foreign_key), + # INDEX I18N_LOCALE_MODEL(locale, model), + # INDEX I18N_FIELD(model, foreign_key, field), + # INDEX I18N_ROW(model, foreign_key), + INDEX locale (locale), + INDEX model (model), + INDEX row_id (foreign_key), + INDEX field (field) ); \ No newline at end of file diff --git a/lib/Cake/Console/Templates/skel/Config/Schema/sessions.php b/lib/Cake/Console/Templates/skel/Config/Schema/sessions.php index b766ebff..2f3612d7 100755 --- a/lib/Cake/Console/Templates/skel/Config/Schema/sessions.php +++ b/lib/Cake/Console/Templates/skel/Config/Schema/sessions.php @@ -22,44 +22,46 @@ * Using the Schema command line utility * cake schema run create Sessions */ -class SessionsSchema extends CakeSchema { +class SessionsSchema extends CakeSchema +{ -/** - * Name property - * - * @var string - */ - public $name = 'Sessions'; + /** + * Name property + * + * @var string + */ + public $name = 'Sessions'; + /** + * The cake_sessions table definition + * + * @var array + */ + public $cake_sessions = [ + 'id' => ['type' => 'string', 'null' => false, 'key' => 'primary'], + 'data' => ['type' => 'text', 'null' => true, 'default' => null], + 'expires' => ['type' => 'integer', 'null' => true, 'default' => null], + 'indexes' => ['PRIMARY' => ['column' => 'id', 'unique' => 1]] + ]; -/** - * Before callback. - * - * @param array $event Schema object properties - * @return bool Should process continue - */ - public function before($event = array()) { - return true; - } + /** + * Before callback. + * + * @param array $event Schema object properties + * @return bool Should process continue + */ + public function before($event = []) + { + return true; + } -/** - * After callback. - * - * @param array $event Schema object properties - * @return void - */ - public function after($event = array()) { - } - -/** - * The cake_sessions table definition - * - * @var array - */ - public $cake_sessions = array( - 'id' => array('type' => 'string', 'null' => false, 'key' => 'primary'), - 'data' => array('type' => 'text', 'null' => true, 'default' => null), - 'expires' => array('type' => 'integer', 'null' => true, 'default' => null), - 'indexes' => array('PRIMARY' => array('column' => 'id', 'unique' => 1)) - ); + /** + * After callback. + * + * @param array $event Schema object properties + * @return void + */ + public function after($event = []) + { + } } diff --git a/lib/Cake/Console/Templates/skel/Config/Schema/sessions.sql b/lib/Cake/Console/Templates/skel/Config/Schema/sessions.sql index e1975562..d19b85bf 100755 --- a/lib/Cake/Console/Templates/skel/Config/Schema/sessions.sql +++ b/lib/Cake/Console/Templates/skel/Config/Schema/sessions.sql @@ -1,4 +1,5 @@ -# $Id$ +# +$Id$ # # Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org) # 1785 E. Sahara Avenue, Suite 490-204 @@ -9,9 +10,10 @@ # Redistributions of files must retain the above copyright notice. # MIT License (https://opensource.org/licenses/mit-license.php) -CREATE TABLE cake_sessions ( - id varchar(255) NOT NULL default '', - data text, - expires int(11) default NULL, - PRIMARY KEY (id) +CREATE TABLE cake_sessions +( + id varchar(255) NOT NULL default '', + data text, + expires int(11) default NULL, + PRIMARY KEY (id) ); \ No newline at end of file diff --git a/lib/Cake/Console/Templates/skel/Config/acl.php b/lib/Cake/Console/Templates/skel/Config/acl.php index 2fe93fea..266e08d2 100755 --- a/lib/Cake/Console/Templates/skel/Config/acl.php +++ b/lib/Cake/Console/Templates/skel/Config/acl.php @@ -64,16 +64,16 @@ * * $config['rules'] = array( * 'allow' => array( - * '*' => 'Role/admin', - * 'controllers/users/(dashboard|profile)' => 'Role/default', - * 'controllers/invoices/*' => 'Role/accountant', - * 'controllers/articles/*' => 'Role/editor', - * 'controllers/users/*' => 'Role/manager', - * 'controllers/invoices/delete' => 'Role/manager', + * '*' => 'Role/admin', + * 'controllers/users/(dashboard|profile)' => 'Role/default', + * 'controllers/invoices/*' => 'Role/accountant', + * 'controllers/articles/*' => 'Role/editor', + * 'controllers/users/*' => 'Role/manager', + * 'controllers/invoices/delete' => 'Role/manager', * ), * 'deny' => array( - * 'controllers/invoices/delete' => 'Role/accountant, User/jeff', - * 'controllers/articles/(delete|publish)' => 'Role/editor', + * 'controllers/invoices/delete' => 'Role/accountant, User/jeff', + * 'controllers/articles/(delete|publish)' => 'Role/editor', * ), * ); * @@ -93,32 +93,32 @@ * The role map defines how to resolve the user record from your application * to the roles you defined in the roles configuration. */ -$config['map'] = array( - 'User' => 'User/username', - 'Role' => 'User/group_id', -); +$config['map'] = [ + 'User' => 'User/username', + 'Role' => 'User/group_id', +]; /** * define aliases to map your model information to * the roles defined in your role configuration. */ -$config['alias'] = array( - 'Role/4' => 'Role/editor', -); +$config['alias'] = [ + 'Role/4' => 'Role/editor', +]; /** * role configuration */ -$config['roles'] = array( - 'Role/admin' => null, -); +$config['roles'] = [ + 'Role/admin' => null, +]; /** * rule configuration */ -$config['rules'] = array( - 'allow' => array( - '*' => 'Role/admin', - ), - 'deny' => array(), -); +$config['rules'] = [ + 'allow' => [ + '*' => 'Role/admin', + ], + 'deny' => [], +]; diff --git a/lib/Cake/Console/Templates/skel/Config/bootstrap.php b/lib/Cake/Console/Templates/skel/Config/bootstrap.php index 6d05aabb..7a7e916c 100755 --- a/lib/Cake/Console/Templates/skel/Config/bootstrap.php +++ b/lib/Cake/Console/Templates/skel/Config/bootstrap.php @@ -14,7 +14,7 @@ */ // Setup a 'default' cache configuration for use in the application. -Cache::config('default', array('engine' => 'File')); +Cache::config('default', ['engine' => 'File']); /** * The settings below can be used to set additional paths to models, views and controllers. @@ -67,29 +67,29 @@ * Feel free to remove or add filters as you see fit for your application. A few examples: * * Configure::write('Dispatcher.filters', array( - * 'MyCacheFilter', // will use MyCacheFilter class from the Routing/Filter package in your app. - * 'MyPlugin.MyFilter', // will use MyFilter class from the Routing/Filter package in MyPlugin plugin. - * array('callable' => $aFunction, 'on' => 'before', 'priority' => 9), // A valid PHP callback type to be called on beforeDispatch - * array('callable' => $anotherMethod, 'on' => 'after'), // A valid PHP callback type to be called on afterDispatch + * 'MyCacheFilter', // will use MyCacheFilter class from the Routing/Filter package in your app. + * 'MyPlugin.MyFilter', // will use MyFilter class from the Routing/Filter package in MyPlugin plugin. + * array('callable' => $aFunction, 'on' => 'before', 'priority' => 9), // A valid PHP callback type to be called on beforeDispatch + * array('callable' => $anotherMethod, 'on' => 'after'), // A valid PHP callback type to be called on afterDispatch * * )); */ -Configure::write('Dispatcher.filters', array( - 'AssetDispatcher', - 'CacheDispatcher' -)); +Configure::write('Dispatcher.filters', [ + 'AssetDispatcher', + 'CacheDispatcher' +]); /** * Configures default file logging options */ App::uses('CakeLog', 'Log'); -CakeLog::config('debug', array( - 'engine' => 'File', - 'types' => array('notice', 'info', 'debug'), - 'file' => 'debug', -)); -CakeLog::config('error', array( - 'engine' => 'File', - 'types' => array('warning', 'error', 'critical', 'alert', 'emergency'), - 'file' => 'error', -)); +CakeLog::config('debug', [ + 'engine' => 'File', + 'types' => ['notice', 'info', 'debug'], + 'file' => 'debug', +]); +CakeLog::config('error', [ + 'engine' => 'File', + 'types' => ['warning', 'error', 'critical', 'alert', 'emergency'], + 'file' => 'error', +]); diff --git a/lib/Cake/Console/Templates/skel/Config/core.php b/lib/Cake/Console/Templates/skel/Config/core.php index 86477766..bdc757b4 100755 --- a/lib/Cake/Console/Templates/skel/Config/core.php +++ b/lib/Cake/Console/Templates/skel/Config/core.php @@ -13,16 +13,16 @@ * CakePHP Debug Level: * * Production Mode: - * 0: No error messages, errors, or warnings shown. Flash messages redirect. + * 0: No error messages, errors, or warnings shown. Flash messages redirect. * * Development Mode: - * 1: Errors and warnings shown, model caches refreshed, flash messages halted. - * 2: As in 1, but also with full debug messages and SQL output. + * 1: Errors and warnings shown, model caches refreshed, flash messages halted. + * 2: As in 1, but also with full debug messages and SQL output. * * In production mode, flash messages redirect after a time interval. * In development mode, you need to click the flash message to continue. */ - Configure::write('debug', 2); +Configure::write('debug', 2); /** * Configure the Error handler used to handle errors for your application. By default @@ -39,11 +39,11 @@ * * @see ErrorHandler for more information on error handling and configuration. */ - Configure::write('Error', array( - 'handler' => 'ErrorHandler::handleError', - 'level' => E_ALL & ~E_DEPRECATED, - 'trace' => true - )); +Configure::write('Error', [ + 'handler' => 'ErrorHandler::handleError', + 'level' => E_ALL & ~E_DEPRECATED, + 'trace' => true +]); /** * Configure the Exception handler used for uncaught exceptions. By default, @@ -65,16 +65,16 @@ * * @see ErrorHandler for more information on exception handling and configuration. */ - Configure::write('Exception', array( - 'handler' => 'ErrorHandler::handleException', - 'renderer' => 'ExceptionRenderer', - 'log' => true - )); +Configure::write('Exception', [ + 'handler' => 'ErrorHandler::handleException', + 'renderer' => 'ExceptionRenderer', + 'log' => true +]); /** * Application wide charset encoding */ - Configure::write('App.encoding', 'UTF-8'); +Configure::write('App.encoding', 'UTF-8'); /** * To configure CakePHP *not* to use mod_rewrite and to @@ -95,7 +95,7 @@ * included primarily as a development convenience - and * thus not recommended for production applications. */ - //Configure::write('App.baseUrl', env('SCRIPT_NAME')); +//Configure::write('App.baseUrl', env('SCRIPT_NAME')); /** * To configure CakePHP to use a particular domain URL @@ -104,25 +104,25 @@ * will override the automatic detection of full base URL and can be * useful when generating links from the CLI (e.g. sending emails) */ - //Configure::write('App.fullBaseUrl', 'https://example.com'); +//Configure::write('App.fullBaseUrl', 'https://example.com'); /** * Web path to the public images directory under webroot. * If not set defaults to 'img/' */ - //Configure::write('App.imageBaseUrl', 'img/'); +//Configure::write('App.imageBaseUrl', 'img/'); /** * Web path to the CSS files directory under webroot. * If not set defaults to 'css/' */ - //Configure::write('App.cssBaseUrl', 'css/'); +//Configure::write('App.cssBaseUrl', 'css/'); /** * Web path to the js files directory under webroot. * If not set defaults to 'js/' */ - //Configure::write('App.jsBaseUrl', 'js/'); +//Configure::write('App.jsBaseUrl', 'js/'); /** * Uncomment the define below to use CakePHP prefix routes. @@ -133,18 +133,18 @@ * Set to an array of prefixes you want to use in your application. Use for * admin or other prefixed routes. * - * Routing.prefixes = array('admin', 'manager'); + * Routing.prefixes = array('admin', 'manager'); * * Enables: - * `admin_index()` and `/admin/controller/index` - * `manager_index()` and `/manager/controller/index` + * `admin_index()` and `/admin/controller/index` + * `manager_index()` and `/manager/controller/index` */ - //Configure::write('Routing.prefixes', array('admin')); +//Configure::write('Routing.prefixes', array('admin')); /** * Turn off all caching application-wide. */ - //Configure::write('Cache.disable', true); +//Configure::write('Cache.disable', true); /** * Enable cache checking. @@ -154,7 +154,7 @@ * You can either set it controller-wide by setting public $cacheAction = true, * or in each action using $this->cacheAction = true. */ - //Configure::write('Cache.check', true); +//Configure::write('Cache.check', true); /** * Enable cache view prefixes. @@ -164,7 +164,7 @@ * for instance. Each version can then have its own view cache namespace. * Note: The final cache file name will then be `prefix_cachefilename`. */ - //Configure::write('Cache.viewPrefix', 'prefix'); +//Configure::write('Cache.viewPrefix', 'prefix'); /** * Session configuration. @@ -202,19 +202,19 @@ * To use database sessions, run the app/Config/Schema/sessions.php schema using * the cake shell command: cake schema create Sessions */ - Configure::write('Session', array( - 'defaults' => 'php' - )); +Configure::write('Session', [ + 'defaults' => 'php' +]); /** * A random string used in security hashing methods. */ - Configure::write('Security.salt', 'DYhG93b0qyJfIxfs2guVoUubWwvniR2G0FgaC9mi'); +Configure::write('Security.salt', 'DYhG93b0qyJfIxfs2guVoUubWwvniR2G0FgaC9mi'); /** * A random numeric string (digits only) used to encrypt/decrypt strings. */ - Configure::write('Security.cipherSeed', '76859309657453542496749683645'); +Configure::write('Security.cipherSeed', '76859309657453542496749683645'); /** * Apply timestamps with the last modified time to static assets (js, css, images). @@ -224,7 +224,7 @@ * Set to `true` to apply timestamps when debug > 0. Set to 'force' to always enable * timestamping regardless of debug value. */ - //Configure::write('Asset.timestamp', true); +//Configure::write('Asset.timestamp', true); /** * Compress CSS output by removing comments, whitespace, repeating tags, etc. @@ -233,7 +233,7 @@ * * To use, prefix the CSS link URL with '/ccss/' instead of '/css/' or use HtmlHelper::css(). */ - //Configure::write('Asset.filter.css', 'css.php'); +//Configure::write('Asset.filter.css', 'css.php'); /** * Plug in your own custom JavaScript compressor by dropping a script in your webroot to handle the @@ -241,20 +241,20 @@ * * To use, prefix your JavaScript link URLs with '/cjs/' instead of '/js/' or use JsHelper::link(). */ - //Configure::write('Asset.filter.js', 'custom_javascript_output_filter.php'); +//Configure::write('Asset.filter.js', 'custom_javascript_output_filter.php'); /** * The class name and database used in CakePHP's * access control lists. */ - Configure::write('Acl.classname', 'DbAcl'); - Configure::write('Acl.database', 'default'); +Configure::write('Acl.classname', 'DbAcl'); +Configure::write('Acl.database', 'default'); /** * Uncomment this line and correct your server timezone to fix * any date & time related errors. */ - //date_default_timezone_set('UTC'); +//date_default_timezone_set('UTC'); /** * Cache Engine Configuration @@ -262,59 +262,59 @@ * * File storage engine. * - * Cache::config('default', array( - * 'engine' => 'File', //[required] - * 'duration' => 3600, //[optional] - * 'probability' => 100, //[optional] - * 'path' => CACHE, //[optional] use system tmp directory - remember to use absolute path - * 'prefix' => 'cake_', //[optional] prefix every cache file with this string - * 'lock' => false, //[optional] use file locking - * 'serialize' => true, //[optional] - * 'mask' => 0664, //[optional] - * )); + * Cache::config('default', array( + * 'engine' => 'File', //[required] + * 'duration' => 3600, //[optional] + * 'probability' => 100, //[optional] + * 'path' => CACHE, //[optional] use system tmp directory - remember to use absolute path + * 'prefix' => 'cake_', //[optional] prefix every cache file with this string + * 'lock' => false, //[optional] use file locking + * 'serialize' => true, //[optional] + * 'mask' => 0664, //[optional] + * )); * * APC (https://pecl.php.net/package/APC) * - * Cache::config('default', array( - * 'engine' => 'Apc', //[required] - * 'duration' => 3600, //[optional] - * 'probability' => 100, //[optional] - * 'prefix' => Inflector::slug(APP_DIR) . '_', //[optional] prefix every cache file with this string - * )); + * Cache::config('default', array( + * 'engine' => 'Apc', //[required] + * 'duration' => 3600, //[optional] + * 'probability' => 100, //[optional] + * 'prefix' => Inflector::slug(APP_DIR) . '_', //[optional] prefix every cache file with this string + * )); * * Xcache (https://xcache.lighttpd.net/) * - * Cache::config('default', array( - * 'engine' => 'Xcache', //[required] - * 'duration' => 3600, //[optional] - * 'probability' => 100, //[optional] - * 'prefix' => Inflector::slug(APP_DIR) . '_', //[optional] prefix every cache file with this string - * 'user' => 'user', //user from xcache.admin.user settings - * 'password' => 'password', //plaintext password (xcache.admin.pass) - * )); + * Cache::config('default', array( + * 'engine' => 'Xcache', //[required] + * 'duration' => 3600, //[optional] + * 'probability' => 100, //[optional] + * 'prefix' => Inflector::slug(APP_DIR) . '_', //[optional] prefix every cache file with this string + * 'user' => 'user', //user from xcache.admin.user settings + * 'password' => 'password', //plaintext password (xcache.admin.pass) + * )); * * Memcache (http://www.danga.com/memcached/) * - * Cache::config('default', array( - * 'engine' => 'Memcache', //[required] - * 'duration' => 3600, //[optional] - * 'probability' => 100, //[optional] - * 'prefix' => Inflector::slug(APP_DIR) . '_', //[optional] prefix every cache file with this string - * 'servers' => array( - * '127.0.0.1:11211' // localhost, default port 11211 - * ), //[optional] - * 'persistent' => true, // [optional] set this to false for non-persistent connections - * 'compress' => false, // [optional] compress data in Memcache (slower, but uses less memory) - * )); + * Cache::config('default', array( + * 'engine' => 'Memcache', //[required] + * 'duration' => 3600, //[optional] + * 'probability' => 100, //[optional] + * 'prefix' => Inflector::slug(APP_DIR) . '_', //[optional] prefix every cache file with this string + * 'servers' => array( + * '127.0.0.1:11211' // localhost, default port 11211 + * ), //[optional] + * 'persistent' => true, // [optional] set this to false for non-persistent connections + * 'compress' => false, // [optional] compress data in Memcache (slower, but uses less memory) + * )); * * Wincache (https://secure.php.net/wincache) * - * Cache::config('default', array( - * 'engine' => 'Wincache', //[required] - * 'duration' => 3600, //[optional] - * 'probability' => 100, //[optional] - * 'prefix' => Inflector::slug(APP_DIR) . '_', //[optional] prefix every cache file with this string - * )); + * Cache::config('default', array( + * 'engine' => 'Wincache', //[required] + * 'duration' => 3600, //[optional] + * 'probability' => 100, //[optional] + * 'prefix' => Inflector::slug(APP_DIR) . '_', //[optional] prefix every cache file with this string + * )); */ /** @@ -332,7 +332,7 @@ // In development mode, caches should expire quickly. $duration = '+999 days'; if (Configure::read('debug') > 0) { - $duration = '+10 seconds'; + $duration = '+10 seconds'; } // Prefix each application on the same server with a different string, to avoid Memcache and APC conflicts. @@ -342,22 +342,22 @@ * Configure the cache used for general framework caching. Path information, * object listings, and translation cache files are stored with this configuration. */ -Cache::config('_cake_core_', array( - 'engine' => $engine, - 'prefix' => $prefix . 'cake_core_', - 'path' => CACHE . 'persistent' . DS, - 'serialize' => ($engine === 'File'), - 'duration' => $duration -)); +Cache::config('_cake_core_', [ + 'engine' => $engine, + 'prefix' => $prefix . 'cake_core_', + 'path' => CACHE . 'persistent' . DS, + 'serialize' => ($engine === 'File'), + 'duration' => $duration +]); /** * Configure the cache for model and datasource caches. This cache configuration * is used to store schema descriptions, and table listings in connections. */ -Cache::config('_cake_model_', array( - 'engine' => $engine, - 'prefix' => $prefix . 'cake_model_', - 'path' => CACHE . 'models' . DS, - 'serialize' => ($engine === 'File'), - 'duration' => $duration -)); +Cache::config('_cake_model_', [ + 'engine' => $engine, + 'prefix' => $prefix . 'cake_model_', + 'path' => CACHE . 'models' . DS, + 'serialize' => ($engine === 'File'), + 'duration' => $duration +]); diff --git a/lib/Cake/Console/Templates/skel/Config/routes.php b/lib/Cake/Console/Templates/skel/Config/routes.php index ac19d9f5..7dc8f19f 100755 --- a/lib/Cake/Console/Templates/skel/Config/routes.php +++ b/lib/Cake/Console/Templates/skel/Config/routes.php @@ -16,20 +16,20 @@ * its action called 'display', and we pass a param to select the view file * to use (in this case, /app/View/Pages/home.ctp)... */ - Router::connect('/', array('controller' => 'pages', 'action' => 'display', 'home')); +Router::connect('/', ['controller' => 'pages', 'action' => 'display', 'home']); /** * ...and connect the rest of 'Pages' controller's URLs. */ - Router::connect('/pages/*', array('controller' => 'pages', 'action' => 'display')); +Router::connect('/pages/*', ['controller' => 'pages', 'action' => 'display']); /** * Load all plugin routes. See the CakePlugin documentation on * how to customize the loading of plugin routes. */ - CakePlugin::routes(); +CakePlugin::routes(); /** * Load the CakePHP default routes. Only remove this if you do not want to use * the built-in default routes. */ - require CAKE . 'Config' . DS . 'routes.php'; +require CAKE . 'Config' . DS . 'routes.php'; diff --git a/lib/Cake/Console/Templates/skel/Console/Command/AppShell.php b/lib/Cake/Console/Templates/skel/Console/Command/AppShell.php index f73c35af..ba0fd8c3 100755 --- a/lib/Cake/Console/Templates/skel/Console/Command/AppShell.php +++ b/lib/Cake/Console/Templates/skel/Console/Command/AppShell.php @@ -16,6 +16,7 @@ * * @package app.Console.Command */ -class AppShell extends Shell { +class AppShell extends Shell +{ } diff --git a/lib/Cake/Console/Templates/skel/Console/cake.php b/lib/Cake/Console/Templates/skel/Console/cake.php index 280613a2..a026f8c9 100755 --- a/lib/Cake/Console/Templates/skel/Console/cake.php +++ b/lib/Cake/Console/Templates/skel/Console/cake.php @@ -17,31 +17,31 @@ */ if (!defined('DS')) { - define('DS', DIRECTORY_SEPARATOR); + define('DS', DIRECTORY_SEPARATOR); } $dispatcher = 'Cake' . DS . 'Console' . DS . 'ShellDispatcher.php'; if (function_exists('ini_set')) { - $root = dirname(dirname(dirname(__FILE__))); - $appDir = basename(dirname(dirname(__FILE__))); - $install = $root . DS . 'lib'; - $composerInstall = $root . DS . $appDir . DS . 'Vendor' . DS . 'cakephp' . DS . 'cakephp' . DS . 'lib'; + $root = dirname(dirname(dirname(__FILE__))); + $appDir = basename(dirname(dirname(__FILE__))); + $install = $root . DS . 'lib'; + $composerInstall = $root . DS . $appDir . DS . 'Vendor' . DS . 'cakephp' . DS . 'cakephp' . DS . 'lib'; - // the following lines differ from its sibling - // /app/Console/cake.php - if (file_exists($composerInstall . DS . $dispatcher)) { - $install = $composerInstall; - } elseif (!file_exists($install . DS . $dispatcher)) { - $install = $root . PATH_SEPARATOR . __CAKE_PATH__; - } + // the following lines differ from its sibling + // /app/Console/cake.php + if (file_exists($composerInstall . DS . $dispatcher)) { + $install = $composerInstall; + } else if (!file_exists($install . DS . $dispatcher)) { + $install = $root . PATH_SEPARATOR . __CAKE_PATH__; + } - ini_set('include_path', $install . PATH_SEPARATOR . ini_get('include_path')); - unset($root, $appDir, $install, $composerInstall); + ini_set('include_path', $install . PATH_SEPARATOR . ini_get('include_path')); + unset($root, $appDir, $install, $composerInstall); } if (!include $dispatcher) { - trigger_error('Could not locate CakePHP core files.', E_USER_ERROR); + trigger_error('Could not locate CakePHP core files.', E_USER_ERROR); } unset($dispatcher); diff --git a/lib/Cake/Console/Templates/skel/Controller/AppController.php b/lib/Cake/Console/Templates/skel/Controller/AppController.php index 4e927f69..d3ca3477 100755 --- a/lib/Cake/Console/Templates/skel/Controller/AppController.php +++ b/lib/Cake/Console/Templates/skel/Controller/AppController.php @@ -18,8 +18,9 @@ * Add your application-wide methods in the class below, your controllers * will inherit them. * - * @package app.Controller - * @link https://book.cakephp.org/2.0/en/controllers.html#the-app-controller + * @package app.Controller + * @link https://book.cakephp.org/2.0/en/controllers.html#the-app-controller */ -class AppController extends Controller { +class AppController extends Controller +{ } diff --git a/lib/Cake/Console/Templates/skel/Controller/PagesController.php b/lib/Cake/Console/Templates/skel/Controller/PagesController.php index 75df741d..cce50645 100755 --- a/lib/Cake/Console/Templates/skel/Controller/PagesController.php +++ b/lib/Cake/Console/Templates/skel/Controller/PagesController.php @@ -19,53 +19,55 @@ * @package app.Controller * @link https://book.cakephp.org/2.0/en/controllers/pages-controller.html */ -class PagesController extends AppController { +class PagesController extends AppController +{ -/** - * This controller does not use a model - * - * @var array - */ - public $uses = array(); + /** + * This controller does not use a model + * + * @var array + */ + public $uses = []; -/** - * Displays a view - * - * @return void - * @throws ForbiddenException When a directory traversal attempt. - * @throws NotFoundException When the view file could not be found - * or MissingViewException in debug mode. - */ - public function display() { - $path = func_get_args(); + /** + * Displays a view + * + * @return void + * @throws ForbiddenException When a directory traversal attempt. + * @throws NotFoundException When the view file could not be found + * or MissingViewException in debug mode. + */ + public function display() + { + $path = func_get_args(); - $count = count($path); - if (!$count) { - return $this->redirect('/'); - } - if (in_array('..', $path, true) || in_array('.', $path, true)) { - throw new ForbiddenException(); - } - $page = $subpage = $title_for_layout = null; + $count = count($path); + if (!$count) { + return $this->redirect('/'); + } + if (in_array('..', $path, true) || in_array('.', $path, true)) { + throw new ForbiddenException(); + } + $page = $subpage = $title_for_layout = null; - if (!empty($path[0])) { - $page = $path[0]; - } - if (!empty($path[1])) { - $subpage = $path[1]; - } - if (!empty($path[$count - 1])) { - $title_for_layout = Inflector::humanize($path[$count - 1]); - } - $this->set(compact('page', 'subpage', 'title_for_layout')); + if (!empty($path[0])) { + $page = $path[0]; + } + if (!empty($path[1])) { + $subpage = $path[1]; + } + if (!empty($path[$count - 1])) { + $title_for_layout = Inflector::humanize($path[$count - 1]); + } + $this->set(compact('page', 'subpage', 'title_for_layout')); - try { - $this->render(implode('/', $path)); - } catch (MissingViewException $e) { - if (Configure::read('debug')) { - throw $e; - } - throw new NotFoundException(); - } - } + try { + $this->render(implode('/', $path)); + } catch (MissingViewException $e) { + if (Configure::read('debug')) { + throw $e; + } + throw new NotFoundException(); + } + } } diff --git a/lib/Cake/Console/Templates/skel/Model/AppModel.php b/lib/Cake/Console/Templates/skel/Model/AppModel.php index cc5ca8fb..0c22ea8c 100755 --- a/lib/Cake/Console/Templates/skel/Model/AppModel.php +++ b/lib/Cake/Console/Templates/skel/Model/AppModel.php @@ -20,5 +20,6 @@ * * @package app.Model */ -class AppModel extends Model { +class AppModel extends Model +{ } diff --git a/lib/Cake/Console/Templates/skel/Test/Case/AllTestsTest.php b/lib/Cake/Console/Templates/skel/Test/Case/AllTestsTest.php index 98b803cc..39083e3a 100755 --- a/lib/Cake/Console/Templates/skel/Test/Case/AllTestsTest.php +++ b/lib/Cake/Console/Templates/skel/Test/Case/AllTestsTest.php @@ -16,16 +16,18 @@ * @license https://opensource.org/licenses/mit-license.php MIT License */ -class AllTestsTest extends CakeTestSuite { +class AllTestsTest extends CakeTestSuite +{ -/** - * Get the suite object. - * - * @return CakeTestSuite Suite class instance. - */ - public static function suite() { - $suite = new CakeTestSuite('All application tests'); - $suite->addTestDirectoryRecursive(TESTS . 'Case'); - return $suite; - } + /** + * Get the suite object. + * + * @return CakeTestSuite Suite class instance. + */ + public static function suite() + { + $suite = new CakeTestSuite('All application tests'); + $suite->addTestDirectoryRecursive(TESTS . 'Case'); + return $suite; + } } diff --git a/lib/Cake/Console/Templates/skel/View/Elements/Flash/default.ctp b/lib/Cake/Console/Templates/skel/View/Elements/Flash/default.ctp index ce0f6135..cf44fa22 100644 --- a/lib/Cake/Console/Templates/skel/View/Elements/Flash/default.ctp +++ b/lib/Cake/Console/Templates/skel/View/Elements/Flash/default.ctp @@ -1 +1,2 @@ -
\ No newline at end of file +
\ No newline at end of file diff --git a/lib/Cake/Console/Templates/skel/View/Emails/html/default.ctp b/lib/Cake/Console/Templates/skel/View/Emails/html/default.ctp index f4dd9e6a..a9b8f4e2 100755 --- a/lib/Cake/Console/Templates/skel/View/Emails/html/default.ctp +++ b/lib/Cake/Console/Templates/skel/View/Emails/html/default.ctp @@ -18,5 +18,5 @@ $content = explode("\n", $content); foreach ($content as $line): - echo '

' . $line . "

\n"; + echo '

' . $line . "

\n"; endforeach; \ No newline at end of file diff --git a/lib/Cake/Console/Templates/skel/View/Errors/error400.ctp b/lib/Cake/Console/Templates/skel/View/Errors/error400.ctp index ff6730d0..09585041 100755 --- a/lib/Cake/Console/Templates/skel/View/Errors/error400.ctp +++ b/lib/Cake/Console/Templates/skel/View/Errors/error400.ctp @@ -5,15 +5,15 @@ * @since CakePHP(tm) v 0.10.0.1076 */ ?> -

-

- : - '{$url}'" - ); ?> -

+

+

+ : + '{$url}'" + ); ?> +

0): - echo $this->element('exception_stack_trace'); + echo $this->element('exception_stack_trace'); endif; diff --git a/lib/Cake/Console/Templates/skel/View/Errors/error500.ctp b/lib/Cake/Console/Templates/skel/View/Errors/error500.ctp index b64ec697..aa6ae041 100755 --- a/lib/Cake/Console/Templates/skel/View/Errors/error500.ctp +++ b/lib/Cake/Console/Templates/skel/View/Errors/error500.ctp @@ -5,12 +5,12 @@ * @since CakePHP(tm) v 0.10.0.1076 */ ?> -

-

- : - -

+

+

+ : + +

0): - echo $this->element('exception_stack_trace'); + echo $this->element('exception_stack_trace'); endif; diff --git a/lib/Cake/Console/Templates/skel/View/Helper/AppHelper.php b/lib/Cake/Console/Templates/skel/View/Helper/AppHelper.php index f9278975..2d2fc482 100755 --- a/lib/Cake/Console/Templates/skel/View/Helper/AppHelper.php +++ b/lib/Cake/Console/Templates/skel/View/Helper/AppHelper.php @@ -20,5 +20,6 @@ * * @package app.View.Helper */ -class AppHelper extends Helper { +class AppHelper extends Helper +{ } diff --git a/lib/Cake/Console/Templates/skel/View/Layouts/Emails/html/default.ctp b/lib/Cake/Console/Templates/skel/View/Layouts/Emails/html/default.ctp index 45dfb27e..7e184e95 100755 --- a/lib/Cake/Console/Templates/skel/View/Layouts/Emails/html/default.ctp +++ b/lib/Cake/Console/Templates/skel/View/Layouts/Emails/html/default.ctp @@ -8,11 +8,11 @@ - <?php echo $this->fetch('title'); ?> + <?php echo $this->fetch('title'); ?> - fetch('content'); ?> +fetch('content'); ?> -

This email was sent using the CakePHP Framework

+

This email was sent using the CakePHP Framework

\ No newline at end of file diff --git a/lib/Cake/Console/Templates/skel/View/Layouts/default.ctp b/lib/Cake/Console/Templates/skel/View/Layouts/default.ctp index 397089b8..fe426b2e 100755 --- a/lib/Cake/Console/Templates/skel/View/Layouts/default.ctp +++ b/lib/Cake/Console/Templates/skel/View/Layouts/default.ctp @@ -10,41 +10,41 @@ $cakeDescription = __d('cake_dev', 'CakePHP: the rapid development php framework - Html->charset(); ?> - - <?php echo $cakeDescription ?>: - <?php echo $this->fetch('title'); ?> - - Html->meta('icon'); + Html->charset(); ?> + + <?php echo $cakeDescription ?>: + <?php echo $this->fetch('title'); ?> + + Html->meta('icon'); - echo $this->Html->css('cake.generic'); + echo $this->Html->css('cake.generic'); - echo $this->fetch('meta'); - echo $this->fetch('css'); - echo $this->fetch('script'); - ?> + echo $this->fetch('meta'); + echo $this->fetch('css'); + echo $this->fetch('script'); + ?> -
- -
+
+ +
- Session->flash(); ?> + Session->flash(); ?> - fetch('content'); ?> -
- -
- element('sql_dump'); ?> + fetch('content'); ?> +
+ +
+element('sql_dump'); ?> diff --git a/lib/Cake/Console/Templates/skel/View/Layouts/error.ctp b/lib/Cake/Console/Templates/skel/View/Layouts/error.ctp index 397089b8..fe426b2e 100755 --- a/lib/Cake/Console/Templates/skel/View/Layouts/error.ctp +++ b/lib/Cake/Console/Templates/skel/View/Layouts/error.ctp @@ -10,41 +10,41 @@ $cakeDescription = __d('cake_dev', 'CakePHP: the rapid development php framework - Html->charset(); ?> - - <?php echo $cakeDescription ?>: - <?php echo $this->fetch('title'); ?> - - Html->meta('icon'); + Html->charset(); ?> + + <?php echo $cakeDescription ?>: + <?php echo $this->fetch('title'); ?> + + Html->meta('icon'); - echo $this->Html->css('cake.generic'); + echo $this->Html->css('cake.generic'); - echo $this->fetch('meta'); - echo $this->fetch('css'); - echo $this->fetch('script'); - ?> + echo $this->fetch('meta'); + echo $this->fetch('css'); + echo $this->fetch('script'); + ?> -
- -
+
+ +
- Session->flash(); ?> + Session->flash(); ?> - fetch('content'); ?> -
- -
- element('sql_dump'); ?> + fetch('content'); ?> +
+ +
+element('sql_dump'); ?> diff --git a/lib/Cake/Console/Templates/skel/View/Layouts/flash.ctp b/lib/Cake/Console/Templates/skel/View/Layouts/flash.ctp index b024cd25..39688fd1 100755 --- a/lib/Cake/Console/Templates/skel/View/Layouts/flash.ctp +++ b/lib/Cake/Console/Templates/skel/View/Layouts/flash.ctp @@ -8,17 +8,29 @@ -Html->charset(); ?> -<?php echo $pageTitle; ?> + Html->charset(); ?> + <?php echo $pageTitle; ?> - - - - + + + +

diff --git a/lib/Cake/Console/Templates/skel/View/Layouts/rss/default.ctp b/lib/Cake/Console/Templates/skel/View/Layouts/rss/default.ctp index 60a53659..a131a9e4 100755 --- a/lib/Cake/Console/Templates/skel/View/Layouts/rss/default.ctp +++ b/lib/Cake/Console/Templates/skel/View/Layouts/rss/default.ctp @@ -1,13 +1,13 @@ fetch('title'); + $channel['title'] = $this->fetch('title'); endif; echo $this->Rss->document( - $this->Rss->channel( - array(), $channel, $this->fetch('content') - ) + $this->Rss->channel( + [], $channel, $this->fetch('content') + ) ); diff --git a/lib/Cake/Console/Templates/skel/View/Pages/home.ctp b/lib/Cake/Console/Templates/skel/View/Pages/home.ctp index 0b261381..e6ad5a96 100755 --- a/lib/Cake/Console/Templates/skel/View/Pages/home.ctp +++ b/lib/Cake/Console/Templates/skel/View/Pages/home.ctp @@ -6,228 +6,277 @@ */ if (!Configure::read('debug')): - throw new NotFoundException(); + throw new NotFoundException(); endif; App::uses('Debugger', 'Utility'); ?>

- +

0): - Debugger::checkSecurityKeys(); + Debugger::checkSecurityKeys(); endif; ?> -

- - 1) Help me configure it - 2) I don't / can't use URL rewriting -

+ ?> +

+ + 1) Help me configure it + 2) I don't / can't use URL rewriting +

-=')): - echo ''; - echo __d('cake_dev', 'Your version of PHP is 5.2.8 or higher.'); - echo ''; - else: - echo ''; - echo __d('cake_dev', 'Your version of PHP is too low. You need PHP 5.2.8 or higher to use CakePHP.'); - echo ''; - endif; -?> + =')): + echo ''; + echo __d('cake_dev', 'Your version of PHP is 5.2.8 or higher.'); + echo ''; + else: + echo ''; + echo __d('cake_dev', 'Your version of PHP is too low. You need PHP 5.2.8 or higher to use CakePHP.'); + echo ''; + endif; + ?>

- '; - echo __d('cake_dev', 'Your tmp directory is writable.'); - echo ''; - else: - echo ''; - echo __d('cake_dev', 'Your tmp directory is NOT writable.'); - echo ''; - endif; - ?> + '; + echo __d('cake_dev', 'Your tmp directory is writable.'); + echo ''; + else: + echo ''; + echo __d('cake_dev', 'Your tmp directory is NOT writable.'); + echo ''; + endif; + ?>

- '; - echo __d('cake_dev', 'The %s is being used for core caching. To change the config edit %s', '' . $settings['engine'] . 'Engine', CONFIG . 'core.php'); - echo ''; - else: - echo ''; - echo __d('cake_dev', 'Your cache is NOT working. Please check the settings in %s', CONFIG . 'core.php'); - echo ''; - endif; - ?> + '; + echo __d('cake_dev', 'The %s is being used for core caching. To change the config edit %s', '' . $settings['engine'] . 'Engine', CONFIG . 'core.php'); + echo ''; + else: + echo ''; + echo __d('cake_dev', 'Your cache is NOT working. Please check the settings in %s', CONFIG . 'core.php'); + echo ''; + endif; + ?>

- '; - echo __d('cake_dev', 'Your database configuration file is present.'); - $filePresent = true; - echo ''; - else: - echo ''; - echo __d('cake_dev', 'Your database configuration file is NOT present.'); - echo '
'; - echo __d('cake_dev', 'Rename %s to %s', CONFIG . 'database.php.default', CONFIG . 'database.php'); - echo '
'; - endif; - ?> + '; + echo __d('cake_dev', 'Your database configuration file is present.'); + $filePresent = true; + echo ''; + else: + echo ''; + echo __d('cake_dev', 'Your database configuration file is NOT present.'); + echo '
'; + echo __d('cake_dev', 'Rename %s to %s', CONFIG . 'database.php.default', CONFIG . 'database.php'); + echo '
'; + endif; + ?>

getMessage(); - if (method_exists($connectionError, 'getAttributes')): - $attributes = $connectionError->getAttributes(); - if (isset($errorMsg['message'])): - $errorMsg .= '
' . $attributes['message']; - endif; - endif; - } -?> -

- isConnected()): - echo ''; - echo __d('cake_dev', 'CakePHP is able to connect to the database.'); - echo ''; - else: - echo ''; - echo __d('cake_dev', 'CakePHP is NOT able to connect to the database.'); - echo '

'; - echo $errorMsg; - echo '
'; - endif; - ?> -

+ App::uses('ConnectionManager', 'Model'); + try { + $connected = ConnectionManager::getDataSource('default'); + } catch (Exception $connectionError) { + $connected = false; + $errorMsg = $connectionError->getMessage(); + if (method_exists($connectionError, 'getAttributes')): + $attributes = $connectionError->getAttributes(); + if (isset($errorMsg['message'])): + $errorMsg .= '
' . $attributes['message']; + endif; + endif; + } + ?> +

+ isConnected()): + echo ''; + echo __d('cake_dev', 'CakePHP is able to connect to the database.'); + echo ''; + else: + echo ''; + echo __d('cake_dev', 'CakePHP is NOT able to connect to the database.'); + echo '

'; + echo $errorMsg; + echo '
'; + endif; + ?> +

'; - echo __d('cake_dev', 'PCRE has not been compiled with Unicode support.'); - echo '
'; - echo __d('cake_dev', 'Recompile PCRE with Unicode support by adding --enable-unicode-properties when configuring'); - echo '

'; - endif; +App::uses('Validation', 'Utility'); +if (!Validation::alphaNumeric('cakephp')): + echo '

'; + echo __d('cake_dev', 'PCRE has not been compiled with Unicode support.'); + echo '
'; + echo __d('cake_dev', 'Recompile PCRE with Unicode support by adding --enable-unicode-properties when configuring'); + echo '

'; +endif; ?>

- '; - echo __d('cake_dev', 'DebugKit plugin is present'); - echo ''; - else: - echo ''; - echo __d('cake_dev', 'DebugKit is not installed. It will help you inspect and debug different aspects of your application.'); - echo '
'; - echo __d('cake_dev', 'You can install it from %s', $this->Html->link('GitHub', 'https://github.com/cakephp/debug_kit/tree/2.2')); - echo '
'; - endif; - ?> + '; + echo __d('cake_dev', 'DebugKit plugin is present'); + echo ''; + else: + echo ''; + echo __d('cake_dev', 'DebugKit is not installed. It will help you inspect and debug different aspects of your application.'); + echo '
'; + echo __d('cake_dev', 'You can install it from %s', $this->Html->link('GitHub', 'https://github.com/cakephp/debug_kit/tree/2.2')); + echo '
'; + endif; + ?>

- + To change its layout, edit: %s.
You can also add some CSS styles for your pages at: %s.', - 'APP/View/Pages/home.ctp', 'APP/View/Layouts/default.ctp', 'APP/webroot/css'); -?> + 'APP/View/Pages/home.ctp', 'APP/View/Layouts/default.ctp', 'APP/webroot/css'); + ?>

- Html->link( - sprintf('%s %s', __d('cake_dev', 'New'), __d('cake_dev', 'CakePHP 2.0 Docs')), - 'https://book.cakephp.org/2.0/en/', - array('target' => '_blank', 'escape' => false) - ); - ?> + Html->link( + sprintf('%s %s', __d('cake_dev', 'New'), __d('cake_dev', 'CakePHP 2.0 Docs')), + 'https://book.cakephp.org/2.0/en/', + ['target' => '_blank', 'escape' => false] + ); + ?>

- Html->link( - __d('cake_dev', 'The 15 min Blog Tutorial'), - 'https://book.cakephp.org/2.0/en/tutorials-and-examples/blog/blog.html', - array('target' => '_blank', 'escape' => false) - ); - ?> + Html->link( + __d('cake_dev', 'The 15 min Blog Tutorial'), + 'https://book.cakephp.org/2.0/en/tutorials-and-examples/blog/blog.html', + ['target' => '_blank', 'escape' => false] + ); + ?>

    -
  • - Html->link('DebugKit', 'https://github.com/cakephp/debug_kit/tree/2.2') ?>: - -
  • -
  • - Html->link('Localized', 'https://github.com/cakephp/localized') ?>: - -
  • +
  • + Html->link('DebugKit', 'https://github.com/cakephp/debug_kit/tree/2.2') ?>: + +
  • +
  • + Html->link('Localized', 'https://github.com/cakephp/localized') ?>: + +

- +

- +

diff --git a/lib/Cake/Console/Templates/skel/webroot/css/cake.generic.css b/lib/Cake/Console/Templates/skel/webroot/css/cake.generic.css index 6d2ad121..0111106b 100755 --- a/lib/Cake/Console/Templates/skel/webroot/css/cake.generic.css +++ b/lib/Cake/Console/Templates/skel/webroot/css/cake.generic.css @@ -16,395 +16,458 @@ */ * { - margin:0; - padding:0; + margin: 0; + padding: 0; } /** General Style Info **/ body { - background: #003d4c; - color: #fff; - font-family:'lucida grande',verdana,helvetica,arial,sans-serif; - font-size:90%; - margin: 0; + background: #003d4c; + color: #fff; + font-family: 'lucida grande', verdana, helvetica, arial, sans-serif; + font-size: 90%; + margin: 0; } + a { - color: #003d4c; - text-decoration: underline; - font-weight: bold; + color: #003d4c; + text-decoration: underline; + font-weight: bold; } + a:hover { - color: #367889; - text-decoration:none; + color: #367889; + text-decoration: none; } + a img { - border:none; + border: none; } + h1, h2, h3, h4 { - font-weight: normal; - margin-bottom:0.5em; + font-weight: normal; + margin-bottom: 0.5em; } + h1 { - background:#fff; - color: #003d4c; - font-size: 100%; + background: #fff; + color: #003d4c; + font-size: 100%; } + h2 { - background:#fff; - color: #e32; - font-family:'Gill Sans','lucida grande', helvetica, arial, sans-serif; - font-size: 190%; + background: #fff; + color: #e32; + font-family: 'Gill Sans', 'lucida grande', helvetica, arial, sans-serif; + font-size: 190%; } + h3 { - color: #2c6877; - font-family:'Gill Sans','lucida grande', helvetica, arial, sans-serif; - font-size: 165%; + color: #2c6877; + font-family: 'Gill Sans', 'lucida grande', helvetica, arial, sans-serif; + font-size: 165%; } + h4 { - color: #993; - font-weight: normal; + color: #993; + font-weight: normal; } + ul, li { - margin: 0 12px; + margin: 0 12px; } + p { - margin: 0 0 1em 0; + margin: 0 0 1em 0; } /** Layout **/ #container { - text-align: left; + text-align: left; } -#header{ - padding: 10px 20px; +#header { + padding: 10px 20px; } + #header h1 { - line-height:20px; - background: #003d4c url('../img/cake.icon.png') no-repeat left; - color: #fff; - padding: 0 30px; + line-height: 20px; + background: #003d4c url('../img/cake.icon.png') no-repeat left; + color: #fff; + padding: 0 30px; } + #header h1 a { - color: #fff; - background: #003d4c; - font-weight: normal; - text-decoration: none; + color: #fff; + background: #003d4c; + font-weight: normal; + text-decoration: none; } + #header h1 a:hover { - color: #fff; - background: #003d4c; - text-decoration: underline; + color: #fff; + background: #003d4c; + text-decoration: underline; } -#content{ - background: #fff; - clear: both; - color: #333; - padding: 10px 20px 40px 20px; - overflow: auto; + +#content { + background: #fff; + clear: both; + color: #333; + padding: 10px 20px 40px 20px; + overflow: auto; } + #footer { - clear: both; - padding: 6px 10px; - text-align: right; + clear: both; + padding: 6px 10px; + text-align: right; } + #header a, #footer a { - color: #fff; + color: #fff; } /** containers **/ div.form, div.index, div.view { - float:right; - width:76%; - border-left:1px solid #666; - padding:10px 2%; + float: right; + width: 76%; + border-left: 1px solid #666; + padding: 10px 2%; } + div.actions { - float:left; - width:16%; - padding:10px 1.5%; + float: left; + width: 16%; + padding: 10px 1.5%; } + div.actions h3 { - padding-top:0; - color:#777; + padding-top: 0; + color: #777; } /** Tables **/ table { - border-right:0; - clear: both; - color: #333; - margin-bottom: 10px; - width: 100%; + border-right: 0; + clear: both; + color: #333; + margin-bottom: 10px; + width: 100%; } + th { - border:0; - border-bottom:2px solid #555; - text-align: left; - padding:4px; + border: 0; + border-bottom: 2px solid #555; + text-align: left; + padding: 4px; } + th a { - display: block; - padding: 2px 4px; - text-decoration: none; + display: block; + padding: 2px 4px; + text-decoration: none; } + th a.asc:after { - content: ' ⇣'; + content: ' ⇣'; } + th a.desc:after { - content: ' ⇡'; + content: ' ⇡'; } + table tr td { - padding: 6px; - text-align: left; - vertical-align: top; - border-bottom:1px solid #ddd; + padding: 6px; + text-align: left; + vertical-align: top; + border-bottom: 1px solid #ddd; } + table tr:nth-child(even) { - background: #f9f9f9; + background: #f9f9f9; } + td.actions { - text-align: center; - white-space: nowrap; + text-align: center; + white-space: nowrap; } + table td.actions a { - margin: 0 6px; - padding:2px 5px; + margin: 0 6px; + padding: 2px 5px; } /* SQL log */ .cake-sql-log { - background: #fff; + background: #fff; } + .cake-sql-log td { - padding: 4px 8px; - text-align: left; - font-family: Monaco, Consolas, "Courier New", monospaced; + padding: 4px 8px; + text-align: left; + font-family: Monaco, Consolas, "Courier New", monospaced; } + .cake-sql-log caption { - color:#fff; + color: #fff; } /** Paging **/ .paging { - background:#fff; - color: #ccc; - margin-top: 1em; - clear:both; + background: #fff; + color: #ccc; + margin-top: 1em; + clear: both; } + .paging .current, .paging .disabled, .paging a { - text-decoration: none; - padding: 5px 8px; - display: inline-block + text-decoration: none; + padding: 5px 8px; + display: inline-block } + .paging > span { - display: inline-block; - border: 1px solid #ccc; - border-left: 0; + display: inline-block; + border: 1px solid #ccc; + border-left: 0; } + .paging > span:hover { - background: #efefef; + background: #efefef; } + .paging .prev { - border-left: 1px solid #ccc; - -moz-border-radius: 4px 0 0 4px; - -webkit-border-radius: 4px 0 0 4px; - border-radius: 4px 0 0 4px; + border-left: 1px solid #ccc; + -moz-border-radius: 4px 0 0 4px; + -webkit-border-radius: 4px 0 0 4px; + border-radius: 4px 0 0 4px; } + .paging .next { - -moz-border-radius: 0 4px 4px 0; - -webkit-border-radius: 0 4px 4px 0; - border-radius: 0 4px 4px 0; + -moz-border-radius: 0 4px 4px 0; + -webkit-border-radius: 0 4px 4px 0; + border-radius: 0 4px 4px 0; } + .paging .disabled { - color: #ddd; + color: #ddd; } + .paging .disabled:hover { - background: transparent; + background: transparent; } + .paging .current { - background: #efefef; - color: #c73e14; + background: #efefef; + color: #c73e14; } /** Scaffold View **/ dl { - line-height: 2em; - margin: 0em 0em; - width: 60%; + line-height: 2em; + margin: 0em 0em; + width: 60%; } + dl dd:nth-child(4n+2), dl dt:nth-child(4n+1) { - background: #f4f4f4; + background: #f4f4f4; } dt { - font-weight: bold; - padding-left: 4px; - vertical-align: top; - width: 10em; + font-weight: bold; + padding-left: 4px; + vertical-align: top; + width: 10em; } + dd { - margin-left: 10em; - margin-top: -2em; - vertical-align: top; + margin-left: 10em; + margin-top: -2em; + vertical-align: top; } /** Forms **/ form { - clear: both; - margin-right: 20px; - padding: 0; - width: 95%; + clear: both; + margin-right: 20px; + padding: 0; + width: 95%; } + fieldset { - border: none; - margin-bottom: 1em; - padding: 16px 10px; + border: none; + margin-bottom: 1em; + padding: 16px 10px; } + fieldset legend { - color: #e32; - font-size: 160%; - font-weight: bold; + color: #e32; + font-size: 160%; + font-weight: bold; } + fieldset fieldset { - margin-top: 0; - padding: 10px 0 0; + margin-top: 0; + padding: 10px 0 0; } + fieldset fieldset legend { - font-size: 120%; - font-weight: normal; + font-size: 120%; + font-weight: normal; } + fieldset fieldset div { - clear: left; - margin: 0 20px; + clear: left; + margin: 0 20px; } + form div { - clear: both; - margin-bottom: 1em; - padding: .5em; - vertical-align: text-top; + clear: both; + margin-bottom: 1em; + padding: .5em; + vertical-align: text-top; } + form .input { - color: #444; + color: #444; } + form .required { - font-weight: bold; + font-weight: bold; } + form .required label:after { - color: #e32; - content: '*'; - display:inline; + color: #e32; + content: '*'; + display: inline; } + form div.submit { - border: 0; - clear: both; - margin-top: 10px; + border: 0; + clear: both; + margin-top: 10px; } + label { - display: block; - font-size: 110%; - margin-bottom:3px; + display: block; + font-size: 110%; + margin-bottom: 3px; } + input, textarea { - clear: both; - font-size: 140%; - font-family: "frutiger linotype", "lucida grande", "verdana", sans-serif; - padding: 1%; - width:98%; + clear: both; + font-size: 140%; + font-family: "frutiger linotype", "lucida grande", "verdana", sans-serif; + padding: 1%; + width: 98%; } + select { - clear: both; - font-size: 120%; - vertical-align: text-bottom; + clear: both; + font-size: 120%; + vertical-align: text-bottom; } + select[multiple=multiple] { - width: 100%; + width: 100%; } + option { - font-size: 120%; - padding: 0 3px; + font-size: 120%; + padding: 0 3px; } + input[type=checkbox] { - clear: left; - float: left; - margin: 0 6px 7px 2px; - width: auto; + clear: left; + float: left; + margin: 0 6px 7px 2px; + width: auto; } + div.checkbox label { - display: inline; + display: inline; } + input[type=radio] { - float:left; - width:auto; - margin: 6px 0; - padding: 0; - line-height: 26px; + float: left; + width: auto; + margin: 6px 0; + padding: 0; + line-height: 26px; } + .radio label { - margin: 0 0 6px 20px; - line-height: 26px; + margin: 0 0 6px 20px; + line-height: 26px; } + input[type=submit] { - display: inline; - font-size: 110%; - width: auto; + display: inline; + font-size: 110%; + width: auto; } + form .submit input[type=submit] { - background:#62af56; - background-image: -webkit-gradient(linear, left top, left bottom, from(#76BF6B), to(#3B8230)); - background-image: -webkit-linear-gradient(top, #76BF6B, #3B8230); - background-image: -moz-linear-gradient(top, #76BF6B, #3B8230); - border-color: #2d6324; - color: #fff; - text-shadow: rgba(0, 0, 0, 0.5) 0 -1px 0; - padding: 8px 10px; + background: #62af56; + background-image: -webkit-gradient(linear, left top, left bottom, from(#76BF6B), to(#3B8230)); + background-image: -webkit-linear-gradient(top, #76BF6B, #3B8230); + background-image: -moz-linear-gradient(top, #76BF6B, #3B8230); + border-color: #2d6324; + color: #fff; + text-shadow: rgba(0, 0, 0, 0.5) 0 -1px 0; + padding: 8px 10px; } + form .submit input[type=submit]:hover { - background: #5BA150; + background: #5BA150; } + /* Form errors */ form .error { - background: #FFDACC; - -moz-border-radius: 4px; - -webkit-border-radius: 4px; - border-radius: 4px; - font-weight: normal; + background: #FFDACC; + -moz-border-radius: 4px; + -webkit-border-radius: 4px; + border-radius: 4px; + font-weight: normal; } + form .error-message { - -moz-border-radius: none; - -webkit-border-radius: none; - border-radius: none; - border: none; - background: none; - margin: 0; - padding-left: 4px; - padding-right: 0; + -moz-border-radius: none; + -webkit-border-radius: none; + border-radius: none; + border: none; + background: none; + margin: 0; + padding-left: 4px; + padding-right: 0; } + form .error, form .error-message { - color: #9E2424; - -webkit-box-shadow: none; - -moz-box-shadow: none; - -ms-box-shadow: none; - -o-box-shadow: none; - box-shadow: none; - text-shadow: none; + color: #9E2424; + -webkit-box-shadow: none; + -moz-box-shadow: none; + -ms-box-shadow: none; + -o-box-shadow: none; + box-shadow: none; + text-shadow: none; } /** Notices and Errors **/ .message { - clear: both; - color: #fff; - font-size: 140%; - font-weight: bold; - margin: 0 0 1em 0; - padding: 5px; + clear: both; + color: #fff; + font-size: 140%; + font-weight: bold; + margin: 0 0 1em 0; + padding: 5px; } .success, @@ -414,328 +477,364 @@ form .error-message { .notice, p.error, .error-message { - background: #ffcc00; - background-repeat: repeat-x; - background-image: -moz-linear-gradient(top, #ffcc00, #E6B800); - background-image: -ms-linear-gradient(top, #ffcc00, #E6B800); - background-image: -webkit-gradient(linear, left top, left bottom, from(#ffcc00), to(#E6B800)); - background-image: -webkit-linear-gradient(top, #ffcc00, #E6B800); - background-image: -o-linear-gradient(top, #ffcc00, #E6B800); - background-image: linear-gradient(top, #ffcc00, #E6B800); - text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); - border: 1px solid rgba(0, 0, 0, 0.2); - margin-bottom: 18px; - padding: 7px 14px; - color: #404040; - text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; - -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25); - -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25); - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25); + background: #ffcc00; + background-repeat: repeat-x; + background-image: -moz-linear-gradient(top, #ffcc00, #E6B800); + background-image: -ms-linear-gradient(top, #ffcc00, #E6B800); + background-image: -webkit-gradient(linear, left top, left bottom, from(#ffcc00), to(#E6B800)); + background-image: -webkit-linear-gradient(top, #ffcc00, #E6B800); + background-image: -o-linear-gradient(top, #ffcc00, #E6B800); + background-image: linear-gradient(top, #ffcc00, #E6B800); + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + border: 1px solid rgba(0, 0, 0, 0.2); + margin-bottom: 18px; + padding: 7px 14px; + color: #404040; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25); + -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25); } + .success, .message, .cake-error, p.error, .error-message { - clear: both; - color: #fff; - background: #c43c35; - border: 1px solid rgba(0, 0, 0, 0.5); - background-repeat: repeat-x; - background-image: -moz-linear-gradient(top, #ee5f5b, #c43c35); - background-image: -ms-linear-gradient(top, #ee5f5b, #c43c35); - background-image: -webkit-gradient(linear, left top, left bottom, from(#ee5f5b), to(#c43c35)); - background-image: -webkit-linear-gradient(top, #ee5f5b, #c43c35); - background-image: -o-linear-gradient(top, #ee5f5b, #c43c35); - background-image: linear-gradient(top, #ee5f5b, #c43c35); - text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.3); + clear: both; + color: #fff; + background: #c43c35; + border: 1px solid rgba(0, 0, 0, 0.5); + background-repeat: repeat-x; + background-image: -moz-linear-gradient(top, #ee5f5b, #c43c35); + background-image: -ms-linear-gradient(top, #ee5f5b, #c43c35); + background-image: -webkit-gradient(linear, left top, left bottom, from(#ee5f5b), to(#c43c35)); + background-image: -webkit-linear-gradient(top, #ee5f5b, #c43c35); + background-image: -o-linear-gradient(top, #ee5f5b, #c43c35); + background-image: linear-gradient(top, #ee5f5b, #c43c35); + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.3); } + .success { - clear: both; - color: #fff; - border: 1px solid rgba(0, 0, 0, 0.5); - background: #3B8230; - background-repeat: repeat-x; - background-image: -webkit-gradient(linear, left top, left bottom, from(#76BF6B), to(#3B8230)); - background-image: -webkit-linear-gradient(top, #76BF6B, #3B8230); - background-image: -moz-linear-gradient(top, #76BF6B, #3B8230); - background-image: -ms-linear-gradient(top, #76BF6B, #3B8230); - background-image: -o-linear-gradient(top, #76BF6B, #3B8230); - background-image: linear-gradient(top, #76BF6B, #3B8230); - text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.3); + clear: both; + color: #fff; + border: 1px solid rgba(0, 0, 0, 0.5); + background: #3B8230; + background-repeat: repeat-x; + background-image: -webkit-gradient(linear, left top, left bottom, from(#76BF6B), to(#3B8230)); + background-image: -webkit-linear-gradient(top, #76BF6B, #3B8230); + background-image: -moz-linear-gradient(top, #76BF6B, #3B8230); + background-image: -ms-linear-gradient(top, #76BF6B, #3B8230); + background-image: -o-linear-gradient(top, #76BF6B, #3B8230); + background-image: linear-gradient(top, #76BF6B, #3B8230); + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.3); } + p.error { - font-family: Monaco, Consolas, Courier, monospace; - font-size: 120%; - padding: 0.8em; - margin: 1em 0; + font-family: Monaco, Consolas, Courier, monospace; + font-size: 120%; + padding: 0.8em; + margin: 1em 0; } + p.error em { - font-weight: normal; - line-height: 140%; + font-weight: normal; + line-height: 140%; } + .notice { - color: #000; - display: block; - font-size: 120%; - padding: 0.8em; - margin: 1em 0; + color: #000; + display: block; + font-size: 120%; + padding: 0.8em; + margin: 1em 0; } + .success { - color: #fff; + color: #fff; } /** Actions **/ .actions ul { - margin: 0; - padding: 0; + margin: 0; + padding: 0; } + .actions li { - margin:0 0 0.5em 0; - list-style-type: none; - white-space: nowrap; - padding: 0; + margin: 0 0 0.5em 0; + list-style-type: none; + white-space: nowrap; + padding: 0; } + .actions ul li a { - font-weight: normal; - display: block; - clear: both; + font-weight: normal; + display: block; + clear: both; } /* Buttons and button links */ input[type=submit], .actions ul li a, .actions a { - font-weight:normal; - padding: 4px 8px; - background: #dcdcdc; - background-image: -webkit-gradient(linear, left top, left bottom, from(#fefefe), to(#dcdcdc)); - background-image: -webkit-linear-gradient(top, #fefefe, #dcdcdc); - background-image: -moz-linear-gradient(top, #fefefe, #dcdcdc); - background-image: -ms-linear-gradient(top, #fefefe, #dcdcdc); - background-image: -o-linear-gradient(top, #fefefe, #dcdcdc); - background-image: linear-gradient(top, #fefefe, #dcdcdc); - color:#333; - border:1px solid #bbb; - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; - text-decoration: none; - text-shadow: #fff 0 1px 0; - min-width: 0; - -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.3), 0 1px 1px rgba(0, 0, 0, 0.2); - -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.3), 0 1px 1px rgba(0, 0, 0, 0.2); - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.3), 0 1px 1px rgba(0, 0, 0, 0.2); - -webkit-user-select: none; - user-select: none; + font-weight: normal; + padding: 4px 8px; + background: #dcdcdc; + background-image: -webkit-gradient(linear, left top, left bottom, from(#fefefe), to(#dcdcdc)); + background-image: -webkit-linear-gradient(top, #fefefe, #dcdcdc); + background-image: -moz-linear-gradient(top, #fefefe, #dcdcdc); + background-image: -ms-linear-gradient(top, #fefefe, #dcdcdc); + background-image: -o-linear-gradient(top, #fefefe, #dcdcdc); + background-image: linear-gradient(top, #fefefe, #dcdcdc); + color: #333; + border: 1px solid #bbb; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + text-decoration: none; + text-shadow: #fff 0 1px 0; + min-width: 0; + -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.3), 0 1px 1px rgba(0, 0, 0, 0.2); + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.3), 0 1px 1px rgba(0, 0, 0, 0.2); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.3), 0 1px 1px rgba(0, 0, 0, 0.2); + -webkit-user-select: none; + user-select: none; } + .actions ul li a:hover, .actions a:hover { - background: #ededed; - border-color: #acacac; - text-decoration: none; + background: #ededed; + border-color: #acacac; + text-decoration: none; } + input[type=submit]:active, .actions ul li a:active, .actions a:active { - background: #eee; - background-image: -webkit-gradient(linear, left top, left bottom, from(#dfdfdf), to(#eee)); - background-image: -webkit-linear-gradient(top, #dfdfdf, #eee); - background-image: -moz-linear-gradient(top, #dfdfdf, #eee); - background-image: -ms-linear-gradient(top, #dfdfdf, #eee); - background-image: -o-linear-gradient(top, #dfdfdf, #eee); - background-image: linear-gradient(top, #dfdfdf, #eee); - text-shadow: #eee 0 1px 0; - -moz-box-shadow: inset 0 1px 4px rgba(0, 0, 0, 0.3); - -webkit-box-shadow: inset 0 1px 4px rgba(0, 0, 0, 0.3); - box-shadow: inset 0 1px 4px rgba(0, 0, 0, 0.3); - border-color: #aaa; - text-decoration: none; + background: #eee; + background-image: -webkit-gradient(linear, left top, left bottom, from(#dfdfdf), to(#eee)); + background-image: -webkit-linear-gradient(top, #dfdfdf, #eee); + background-image: -moz-linear-gradient(top, #dfdfdf, #eee); + background-image: -ms-linear-gradient(top, #dfdfdf, #eee); + background-image: -o-linear-gradient(top, #dfdfdf, #eee); + background-image: linear-gradient(top, #dfdfdf, #eee); + text-shadow: #eee 0 1px 0; + -moz-box-shadow: inset 0 1px 4px rgba(0, 0, 0, 0.3); + -webkit-box-shadow: inset 0 1px 4px rgba(0, 0, 0, 0.3); + box-shadow: inset 0 1px 4px rgba(0, 0, 0, 0.3); + border-color: #aaa; + text-decoration: none; } /** Related **/ .related { - clear: both; - display: block; + clear: both; + display: block; } /** Debugging **/ pre { - color: #000; - background: #f0f0f0; - padding: 15px; - -moz-box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.3); - -webkit-box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.3); - box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.3); + color: #000; + background: #f0f0f0; + padding: 15px; + -moz-box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.3); + -webkit-box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.3); + box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.3); } + .cake-debug-output { - padding: 0; - position: relative; + padding: 0; + position: relative; } + .cake-debug-output > span { - position: absolute; - top: 5px; - right: 5px; - background: rgba(255, 255, 255, 0.3); - -moz-border-radius: 4px; - -webkit-border-radius: 4px; - border-radius: 4px; - padding: 5px 6px; - color: #000; - display: block; - float: left; - -moz-box-shadow: inset 0 1px 0 rgba(0, 0, 0, 0.25), 0 1px 0 rgba(255, 255, 255, 0.5); - -webkit-box-shadow: inset 0 1px 0 rgba(0, 0, 0, 0.25), 0 1px 0 rgba(255, 255, 255, 0.5); - box-shadow: inset 0 1px 0 rgba(0, 0, 0, 0.25), 0 1px 0 rgba(255, 255, 255, 0.5); - text-shadow: 0 1px 1px rgba(255, 255, 255, 0.8); + position: absolute; + top: 5px; + right: 5px; + background: rgba(255, 255, 255, 0.3); + -moz-border-radius: 4px; + -webkit-border-radius: 4px; + border-radius: 4px; + padding: 5px 6px; + color: #000; + display: block; + float: left; + -moz-box-shadow: inset 0 1px 0 rgba(0, 0, 0, 0.25), 0 1px 0 rgba(255, 255, 255, 0.5); + -webkit-box-shadow: inset 0 1px 0 rgba(0, 0, 0, 0.25), 0 1px 0 rgba(255, 255, 255, 0.5); + box-shadow: inset 0 1px 0 rgba(0, 0, 0, 0.25), 0 1px 0 rgba(255, 255, 255, 0.5); + text-shadow: 0 1px 1px rgba(255, 255, 255, 0.8); } + .cake-debug, .cake-error { - font-size: 16px; - line-height: 20px; - clear: both; + font-size: 16px; + line-height: 20px; + clear: both; } + .cake-error > a { - text-shadow: none; + text-shadow: none; } + .cake-error { - white-space: normal; + white-space: normal; } + .cake-stack-trace { - background: rgba(255, 255, 255, 0.7); - color: #333; - margin: 10px 0 5px 0; - padding: 10px 10px 0 10px; - font-size: 120%; - line-height: 140%; - overflow: auto; - position: relative; - -moz-border-radius: 4px; - -webkit-border-radius: 4px; - border-radius: 4px; + background: rgba(255, 255, 255, 0.7); + color: #333; + margin: 10px 0 5px 0; + padding: 10px 10px 0 10px; + font-size: 120%; + line-height: 140%; + overflow: auto; + position: relative; + -moz-border-radius: 4px; + -webkit-border-radius: 4px; + border-radius: 4px; } + .cake-stack-trace a { - text-shadow: none; - background: rgba(255, 255, 255, 0.7); - padding: 5px; - -moz-border-radius: 10px; - -webkit-border-radius: 10px; - border-radius: 10px; - margin: 0 4px 10px 2px; - font-family: sans-serif; - font-size: 14px; - line-height: 14px; - display: inline-block; - text-decoration: none; - -moz-box-shadow: inset 0 1px 0 rgba(0, 0, 0, 0.3); - -webkit-box-shadow: inset 0 1px 0 rgba(0, 0, 0, 0.3); - box-shadow: inset 0 1px 0 rgba(0, 0, 0, 0.3); + text-shadow: none; + background: rgba(255, 255, 255, 0.7); + padding: 5px; + -moz-border-radius: 10px; + -webkit-border-radius: 10px; + border-radius: 10px; + margin: 0 4px 10px 2px; + font-family: sans-serif; + font-size: 14px; + line-height: 14px; + display: inline-block; + text-decoration: none; + -moz-box-shadow: inset 0 1px 0 rgba(0, 0, 0, 0.3); + -webkit-box-shadow: inset 0 1px 0 rgba(0, 0, 0, 0.3); + box-shadow: inset 0 1px 0 rgba(0, 0, 0, 0.3); } + .cake-code-dump pre { - position: relative; - overflow: auto; + position: relative; + overflow: auto; } + .cake-context { - margin-bottom: 10px; + margin-bottom: 10px; } + .cake-stack-trace pre { - color: #000; - background-color: #F0F0F0; - margin: 0 0 10px 0; - padding: 1em; - overflow: auto; - text-shadow: none; + color: #000; + background-color: #F0F0F0; + margin: 0 0 10px 0; + padding: 1em; + overflow: auto; + text-shadow: none; } + .cake-stack-trace li { - padding: 10px 5px 0; - margin: 0 0 4px 0; - font-family: monospace; - border: 1px solid #bbb; - -moz-border-radius: 4px; - -wekbkit-border-radius: 4px; - border-radius: 4px; - background: #dcdcdc; - background-image: -webkit-gradient(linear, left top, left bottom, from(#fefefe), to(#dcdcdc)); - background-image: -webkit-linear-gradient(top, #fefefe, #dcdcdc); - background-image: -moz-linear-gradient(top, #fefefe, #dcdcdc); - background-image: -ms-linear-gradient(top, #fefefe, #dcdcdc); - background-image: -o-linear-gradient(top, #fefefe, #dcdcdc); - background-image: linear-gradient(top, #fefefe, #dcdcdc); + padding: 10px 5px 0; + margin: 0 0 4px 0; + font-family: monospace; + border: 1px solid #bbb; + -moz-border-radius: 4px; + -wekbkit-border-radius: 4px; + border-radius: 4px; + background: #dcdcdc; + background-image: -webkit-gradient(linear, left top, left bottom, from(#fefefe), to(#dcdcdc)); + background-image: -webkit-linear-gradient(top, #fefefe, #dcdcdc); + background-image: -moz-linear-gradient(top, #fefefe, #dcdcdc); + background-image: -ms-linear-gradient(top, #fefefe, #dcdcdc); + background-image: -o-linear-gradient(top, #fefefe, #dcdcdc); + background-image: linear-gradient(top, #fefefe, #dcdcdc); } + /* excerpt */ .cake-code-dump pre, .cake-code-dump pre code { - clear: both; - font-size: 12px; - line-height: 15px; - margin: 4px 2px; - padding: 4px; - overflow: auto; + clear: both; + font-size: 12px; + line-height: 15px; + margin: 4px 2px; + padding: 4px; + overflow: auto; } + .cake-code-dump .code-highlight { - display: block; - background-color: rgba(255, 255, 0, 0.5); + display: block; + background-color: rgba(255, 255, 0, 0.5); } + .code-coverage-results div.code-line { - padding-left:5px; - display:block; - margin-left:10px; + padding-left: 5px; + display: block; + margin-left: 10px; } + .code-coverage-results div.uncovered span.content { - background:#ecc; + background: #ecc; } + .code-coverage-results div.covered span.content { - background:#cec; + background: #cec; } + .code-coverage-results div.ignored span.content { - color:#aaa; + color: #aaa; } + .code-coverage-results span.line-num { - color:#666; - display:block; - float:left; - width:20px; - text-align:right; - margin-right:5px; + color: #666; + display: block; + float: left; + width: 20px; + text-align: right; + margin-right: 5px; } + .code-coverage-results span.line-num strong { - color:#666; + color: #666; } + .code-coverage-results div.start { - border:1px solid #aaa; - border-width:1px 1px 0 1px; - margin-top:30px; - padding-top:5px; + border: 1px solid #aaa; + border-width: 1px 1px 0 1px; + margin-top: 30px; + padding-top: 5px; } + .code-coverage-results div.end { - border:1px solid #aaa; - border-width:0px 1px 1px 1px; - margin-bottom:30px; - padding-bottom:5px; + border: 1px solid #aaa; + border-width: 0px 1px 1px 1px; + margin-bottom: 30px; + padding-bottom: 5px; } + .code-coverage-results div.realstart { - margin-top:0px; + margin-top: 0px; } + .code-coverage-results p.note { - color:#bbb; - padding:5px; - margin:5px 0 10px; - font-size:10px; + color: #bbb; + padding: 5px; + margin: 5px 0 10px; + font-size: 10px; } + .code-coverage-results span.result-bad { - color: #a00; + color: #a00; } + .code-coverage-results span.result-ok { - color: #fa0; + color: #fa0; } + .code-coverage-results span.result-good { - color: #0a0; + color: #0a0; } /** Elements **/ #url-rewriting-warning { - display:none; + display: none; } diff --git a/lib/Cake/Console/Templates/skel/webroot/index.php b/lib/Cake/Console/Templates/skel/webroot/index.php index 4f9a7095..0966c280 100755 --- a/lib/Cake/Console/Templates/skel/webroot/index.php +++ b/lib/Cake/Console/Templates/skel/webroot/index.php @@ -20,7 +20,7 @@ * Use the DS to separate the directories in other defines */ if (!defined('DS')) { - define('DS', DIRECTORY_SEPARATOR); + define('DS', DIRECTORY_SEPARATOR); } /** @@ -33,14 +33,14 @@ * The full path to the directory which holds "app", WITHOUT a trailing DS. */ if (!defined('ROOT')) { - define('ROOT', dirname(dirname(dirname(__FILE__)))); + define('ROOT', dirname(dirname(dirname(__FILE__)))); } /** * The actual directory name for the "app". */ if (!defined('APP_DIR')) { - define('APP_DIR', basename(dirname(dirname(__FILE__)))); + define('APP_DIR', basename(dirname(dirname(__FILE__)))); } /** @@ -66,7 +66,7 @@ $vendorPath = ROOT . DS . APP_DIR . DS . 'Vendor' . DS . 'cakephp' . DS . 'cakephp' . DS . 'lib'; $dispatcher = 'Cake' . DS . 'Console' . DS . 'ShellDispatcher.php'; if (!defined('CAKE_CORE_INCLUDE_PATH') && file_exists($vendorPath . DS . $dispatcher)) { - define('CAKE_CORE_INCLUDE_PATH', $vendorPath); + define('CAKE_CORE_INCLUDE_PATH', $vendorPath); } /** @@ -74,38 +74,38 @@ * Change at your own risk. */ if (!defined('WEBROOT_DIR')) { - define('WEBROOT_DIR', basename(dirname(__FILE__))); + define('WEBROOT_DIR', basename(dirname(__FILE__))); } if (!defined('WWW_ROOT')) { - define('WWW_ROOT', dirname(__FILE__) . DS); + define('WWW_ROOT', dirname(__FILE__) . DS); } // For the built-in server if (PHP_SAPI === 'cli-server') { - if ($_SERVER['REQUEST_URI'] !== '/' && file_exists(WWW_ROOT . $_SERVER['PHP_SELF'])) { - return false; - } - $_SERVER['PHP_SELF'] = '/' . basename(__FILE__); + if ($_SERVER['REQUEST_URI'] !== '/' && file_exists(WWW_ROOT . $_SERVER['PHP_SELF'])) { + return false; + } + $_SERVER['PHP_SELF'] = '/' . basename(__FILE__); } if (!defined('CAKE_CORE_INCLUDE_PATH')) { - if (function_exists('ini_set')) { - ini_set('include_path', ROOT . DS . 'lib' . PATH_SEPARATOR . ini_get('include_path')); - } - if (!include 'Cake' . DS . 'bootstrap.php') { - $failed = true; - } -} elseif (!include CAKE_CORE_INCLUDE_PATH . DS . 'Cake' . DS . 'bootstrap.php') { - $failed = true; + if (function_exists('ini_set')) { + ini_set('include_path', ROOT . DS . 'lib' . PATH_SEPARATOR . ini_get('include_path')); + } + if (!include 'Cake' . DS . 'bootstrap.php') { + $failed = true; + } +} else if (!include CAKE_CORE_INCLUDE_PATH . DS . 'Cake' . DS . 'bootstrap.php') { + $failed = true; } if (!empty($failed)) { - trigger_error("CakePHP core could not be found. Check the value of CAKE_CORE_INCLUDE_PATH in APP/webroot/index.php. It should point to the directory containing your " . DS . "cake core directory and your " . DS . "vendors root directory.", E_USER_ERROR); + trigger_error("CakePHP core could not be found. Check the value of CAKE_CORE_INCLUDE_PATH in APP/webroot/index.php. It should point to the directory containing your " . DS . "cake core directory and your " . DS . "vendors root directory.", E_USER_ERROR); } App::uses('Dispatcher', 'Routing'); $Dispatcher = new Dispatcher(); $Dispatcher->dispatch( - new CakeRequest(), - new CakeResponse() + new CakeRequest(), + new CakeResponse() ); diff --git a/lib/Cake/Console/Templates/skel/webroot/test.php b/lib/Cake/Console/Templates/skel/webroot/test.php index caa29331..8c5c9929 100755 --- a/lib/Cake/Console/Templates/skel/webroot/test.php +++ b/lib/Cake/Console/Templates/skel/webroot/test.php @@ -14,7 +14,7 @@ * Use the DS to separate the directories in other defines */ if (!defined('DS')) { - define('DS', DIRECTORY_SEPARATOR); + define('DS', DIRECTORY_SEPARATOR); } /** @@ -27,14 +27,14 @@ * The full path to the directory which holds "app", WITHOUT a trailing DS. */ if (!defined('ROOT')) { - define('ROOT', dirname(dirname(dirname(__FILE__)))); + define('ROOT', dirname(dirname(dirname(__FILE__)))); } /** * The actual directory name for the "app". */ if (!defined('APP_DIR')) { - define('APP_DIR', basename(dirname(dirname(__FILE__)))); + define('APP_DIR', basename(dirname(dirname(__FILE__)))); } /** @@ -57,7 +57,7 @@ $vendorPath = ROOT . DS . APP_DIR . DS . 'Vendor' . DS . 'cakephp' . DS . 'cakephp' . DS . 'lib'; $dispatcher = 'Cake' . DS . 'Console' . DS . 'ShellDispatcher.php'; if (!defined('CAKE_CORE_INCLUDE_PATH') && file_exists($vendorPath . DS . $dispatcher)) { - define('CAKE_CORE_INCLUDE_PATH', $vendorPath); + define('CAKE_CORE_INCLUDE_PATH', $vendorPath); } /** @@ -65,30 +65,30 @@ * Change at your own risk. */ if (!defined('WEBROOT_DIR')) { - define('WEBROOT_DIR', basename(dirname(__FILE__))); + define('WEBROOT_DIR', basename(dirname(__FILE__))); } if (!defined('WWW_ROOT')) { - define('WWW_ROOT', dirname(__FILE__) . DS); + define('WWW_ROOT', dirname(__FILE__) . DS); } if (!defined('CAKE_CORE_INCLUDE_PATH')) { - if (function_exists('ini_set')) { - ini_set('include_path', ROOT . DS . 'lib' . PATH_SEPARATOR . ini_get('include_path')); - } - if (!include 'Cake' . DS . 'bootstrap.php') { - $failed = true; - } + if (function_exists('ini_set')) { + ini_set('include_path', ROOT . DS . 'lib' . PATH_SEPARATOR . ini_get('include_path')); + } + if (!include 'Cake' . DS . 'bootstrap.php') { + $failed = true; + } } else { - if (!include CAKE_CORE_INCLUDE_PATH . DS . 'Cake' . DS . 'bootstrap.php') { - $failed = true; - } + if (!include CAKE_CORE_INCLUDE_PATH . DS . 'Cake' . DS . 'bootstrap.php') { + $failed = true; + } } if (!empty($failed)) { - trigger_error("CakePHP core could not be found. Check the value of CAKE_CORE_INCLUDE_PATH in APP/webroot/index.php. It should point to the directory containing your " . DS . "cake core directory and your " . DS . "vendors root directory.", E_USER_ERROR); + trigger_error("CakePHP core could not be found. Check the value of CAKE_CORE_INCLUDE_PATH in APP/webroot/index.php. It should point to the directory containing your " . DS . "cake core directory and your " . DS . "vendors root directory.", E_USER_ERROR); } if (Configure::read('debug') < 1) { - throw new NotFoundException(__d('cake_dev', 'Debug setting does not allow access to this URL.')); + throw new NotFoundException(__d('cake_dev', 'Debug setting does not allow access to this URL.')); } require_once CAKE . 'TestSuite' . DS . 'CakeTestSuiteDispatcher.php'; diff --git a/lib/Cake/Console/cake.php b/lib/Cake/Console/cake.php index b825e023..41334e46 100755 --- a/lib/Cake/Console/cake.php +++ b/lib/Cake/Console/cake.php @@ -18,7 +18,7 @@ */ if (!defined('DS')) { - define('DS', DIRECTORY_SEPARATOR); + define('DS', DIRECTORY_SEPARATOR); } $dispatcher = 'Cake' . DS . 'Console' . DS . 'ShellDispatcher.php'; @@ -26,27 +26,27 @@ $paths = explode(PATH_SEPARATOR, ini_get('include_path')); foreach ($paths as $path) { - if (file_exists($path . DS . $dispatcher)) { - $found = $path; - break; - } + if (file_exists($path . DS . $dispatcher)) { + $found = $path; + break; + } } if (!$found) { - $rootInstall = dirname(dirname(dirname(__FILE__))) . DS . $dispatcher; - $composerInstall = dirname(dirname(__FILE__)) . DS . $dispatcher; - - if (file_exists($composerInstall)) { - include $composerInstall; - } elseif (file_exists($rootInstall)) { - include $rootInstall; - } else { - trigger_error('Could not locate CakePHP core files.', E_USER_ERROR); - } - unset($rootInstall, $composerInstall); + $rootInstall = dirname(dirname(dirname(__FILE__))) . DS . $dispatcher; + $composerInstall = dirname(dirname(__FILE__)) . DS . $dispatcher; + + if (file_exists($composerInstall)) { + include $composerInstall; + } else if (file_exists($rootInstall)) { + include $rootInstall; + } else { + trigger_error('Could not locate CakePHP core files.', E_USER_ERROR); + } + unset($rootInstall, $composerInstall); } else { - include $found . DS . $dispatcher; + include $found . DS . $dispatcher; } unset($paths, $path, $found, $dispatcher); diff --git a/lib/Cake/Controller/CakeErrorController.php b/lib/Cake/Controller/CakeErrorController.php index 423dc27b..d8f860e1 100755 --- a/lib/Cake/Controller/CakeErrorController.php +++ b/lib/Cake/Controller/CakeErrorController.php @@ -27,36 +27,38 @@ * * @package Cake.Controller */ -class CakeErrorController extends AppController { +class CakeErrorController extends AppController +{ -/** - * Uses Property - * - * @var array - */ - public $uses = array(); + /** + * Uses Property + * + * @var array + */ + public $uses = []; -/** - * Constructor - * - * @param CakeRequest $request Request instance. - * @param CakeResponse $response Response instance. - */ - public function __construct($request = null, $response = null) { - parent::__construct($request, $response); - $this->constructClasses(); - if (count(Router::extensions()) && - !$this->Components->attached('RequestHandler') - ) { - $this->RequestHandler = $this->Components->load('RequestHandler'); - } - if ($this->Components->enabled('Auth')) { - $this->Components->disable('Auth'); - } - if ($this->Components->enabled('Security')) { - $this->Components->disable('Security'); - } - $this->_set(array('cacheAction' => false, 'viewPath' => 'Errors')); - } + /** + * Constructor + * + * @param CakeRequest $request Request instance. + * @param CakeResponse $response Response instance. + */ + public function __construct($request = null, $response = null) + { + parent::__construct($request, $response); + $this->constructClasses(); + if (count(Router::extensions()) && + !$this->Components->attached('RequestHandler') + ) { + $this->RequestHandler = $this->Components->load('RequestHandler'); + } + if ($this->Components->enabled('Auth')) { + $this->Components->disable('Auth'); + } + if ($this->Components->enabled('Security')) { + $this->Components->disable('Security'); + } + $this->_set(['cacheAction' => false, 'viewPath' => 'Errors']); + } } diff --git a/lib/Cake/Controller/Component.php b/lib/Cake/Controller/Component.php index d13e575e..9f61b632 100755 --- a/lib/Cake/Controller/Component.php +++ b/lib/Cake/Controller/Component.php @@ -37,128 +37,133 @@ * @link https://book.cakephp.org/2.0/en/controllers/components.html * @see Controller::$components */ -class Component extends CakeObject { +class Component extends CakeObject +{ -/** - * Component collection class used to lazy load components. - * - * @var ComponentCollection - */ - protected $_Collection; + /** + * Settings for this Component + * + * @var array + */ + public $settings = []; + /** + * Other Components this component uses. + * + * @var array + */ + public $components = []; + /** + * Component collection class used to lazy load components. + * + * @var ComponentCollection + */ + protected $_Collection; + /** + * A component lookup table used to lazy load component objects. + * + * @var array + */ + protected $_componentMap = []; -/** - * Settings for this Component - * - * @var array - */ - public $settings = array(); + /** + * Constructor + * + * @param ComponentCollection $collection A ComponentCollection this component can use to lazy load its components + * @param array $settings Array of configuration settings. + */ + public function __construct(ComponentCollection $collection, $settings = []) + { + $this->_Collection = $collection; + $this->settings = $settings; + $this->_set($settings); + if (!empty($this->components)) { + $this->_componentMap = ComponentCollection::normalizeObjectArray($this->components); + } + } -/** - * Other Components this component uses. - * - * @var array - */ - public $components = array(); + /** + * Magic method for lazy loading $components. + * + * @param string $name Name of component to get. + * @return mixed A Component object or null. + */ + public function __get($name) + { + if (isset($this->_componentMap[$name]) && !isset($this->{$name})) { + $settings = (array)$this->_componentMap[$name]['settings'] + ['enabled' => false]; + $this->{$name} = $this->_Collection->load($this->_componentMap[$name]['class'], $settings); + } + if (isset($this->{$name})) { + return $this->{$name}; + } + } -/** - * A component lookup table used to lazy load component objects. - * - * @var array - */ - protected $_componentMap = array(); + /** + * Called before the Controller::beforeFilter(). + * + * @param Controller $controller Controller with components to initialize + * @return void + * @link https://book.cakephp.org/2.0/en/controllers/components.html#Component::initialize + */ + public function initialize(Controller $controller) + { + } -/** - * Constructor - * - * @param ComponentCollection $collection A ComponentCollection this component can use to lazy load its components - * @param array $settings Array of configuration settings. - */ - public function __construct(ComponentCollection $collection, $settings = array()) { - $this->_Collection = $collection; - $this->settings = $settings; - $this->_set($settings); - if (!empty($this->components)) { - $this->_componentMap = ComponentCollection::normalizeObjectArray($this->components); - } - } + /** + * Called after the Controller::beforeFilter() and before the controller action + * + * @param Controller $controller Controller with components to startup + * @return void + * @link https://book.cakephp.org/2.0/en/controllers/components.html#Component::startup + */ + public function startup(Controller $controller) + { + } -/** - * Magic method for lazy loading $components. - * - * @param string $name Name of component to get. - * @return mixed A Component object or null. - */ - public function __get($name) { - if (isset($this->_componentMap[$name]) && !isset($this->{$name})) { - $settings = (array)$this->_componentMap[$name]['settings'] + array('enabled' => false); - $this->{$name} = $this->_Collection->load($this->_componentMap[$name]['class'], $settings); - } - if (isset($this->{$name})) { - return $this->{$name}; - } - } + /** + * Called before the Controller::beforeRender(), and before + * the view class is loaded, and before Controller::render() + * + * @param Controller $controller Controller with components to beforeRender + * @return void + * @link https://book.cakephp.org/2.0/en/controllers/components.html#Component::beforeRender + */ + public function beforeRender(Controller $controller) + { + } -/** - * Called before the Controller::beforeFilter(). - * - * @param Controller $controller Controller with components to initialize - * @return void - * @link https://book.cakephp.org/2.0/en/controllers/components.html#Component::initialize - */ - public function initialize(Controller $controller) { - } - -/** - * Called after the Controller::beforeFilter() and before the controller action - * - * @param Controller $controller Controller with components to startup - * @return void - * @link https://book.cakephp.org/2.0/en/controllers/components.html#Component::startup - */ - public function startup(Controller $controller) { - } + /** + * Called after Controller::render() and before the output is printed to the browser. + * + * @param Controller $controller Controller with components to shutdown + * @return void + * @link https://book.cakephp.org/2.0/en/controllers/components.html#Component::shutdown + */ + public function shutdown(Controller $controller) + { + } -/** - * Called before the Controller::beforeRender(), and before - * the view class is loaded, and before Controller::render() - * - * @param Controller $controller Controller with components to beforeRender - * @return void - * @link https://book.cakephp.org/2.0/en/controllers/components.html#Component::beforeRender - */ - public function beforeRender(Controller $controller) { - } - -/** - * Called after Controller::render() and before the output is printed to the browser. - * - * @param Controller $controller Controller with components to shutdown - * @return void - * @link https://book.cakephp.org/2.0/en/controllers/components.html#Component::shutdown - */ - public function shutdown(Controller $controller) { - } - -/** - * Called before Controller::redirect(). Allows you to replace the URL that will - * be redirected to with a new URL. The return of this method can either be an array or a string. - * - * If the return is an array and contains a 'url' key. You may also supply the following: - * - * - `status` The status code for the redirect - * - `exit` Whether or not the redirect should exit. - * - * If your response is a string or an array that does not contain a 'url' key it will - * be used as the new URL to redirect to. - * - * @param Controller $controller Controller with components to beforeRedirect - * @param string|array $url Either the string or URL array that is being redirected to. - * @param int $status The status code of the redirect - * @param bool $exit Will the script exit. - * @return array|null Either an array or null. - * @link https://book.cakephp.org/2.0/en/controllers/components.html#Component::beforeRedirect - */ - public function beforeRedirect(Controller $controller, $url, $status = null, $exit = true) { - } + /** + * Called before Controller::redirect(). Allows you to replace the URL that will + * be redirected to with a new URL. The return of this method can either be an array or a string. + * + * If the return is an array and contains a 'url' key. You may also supply the following: + * + * - `status` The status code for the redirect + * - `exit` Whether or not the redirect should exit. + * + * If your response is a string or an array that does not contain a 'url' key it will + * be used as the new URL to redirect to. + * + * @param Controller $controller Controller with components to beforeRedirect + * @param string|array $url Either the string or URL array that is being redirected to. + * @param int $status The status code of the redirect + * @param bool $exit Will the script exit. + * @return array|null Either an array or null. + * @link https://book.cakephp.org/2.0/en/controllers/components.html#Component::beforeRedirect + */ + public function beforeRedirect(Controller $controller, $url, $status = null, $exit = true) + { + } } diff --git a/lib/Cake/Controller/Component/Acl/AclInterface.php b/lib/Cake/Controller/Component/Acl/AclInterface.php index 6d632417..dd5eb996 100755 --- a/lib/Cake/Controller/Component/Acl/AclInterface.php +++ b/lib/Cake/Controller/Component/Acl/AclInterface.php @@ -20,54 +20,55 @@ * * @package Cake.Controller.Component.Acl */ -interface AclInterface { +interface AclInterface +{ -/** - * Empty method to be overridden in subclasses - * - * @param string $aro ARO The requesting object identifier. - * @param string $aco ACO The controlled object identifier. - * @param string $action Action (defaults to *) - * @return bool Success - */ - public function check($aro, $aco, $action = "*"); + /** + * Empty method to be overridden in subclasses + * + * @param string $aro ARO The requesting object identifier. + * @param string $aco ACO The controlled object identifier. + * @param string $action Action (defaults to *) + * @return bool Success + */ + public function check($aro, $aco, $action = "*"); -/** - * Allow methods are used to grant an ARO access to an ACO. - * - * @param string $aro ARO The requesting object identifier. - * @param string $aco ACO The controlled object identifier. - * @param string $action Action (defaults to *) - * @return bool Success - */ - public function allow($aro, $aco, $action = "*"); + /** + * Allow methods are used to grant an ARO access to an ACO. + * + * @param string $aro ARO The requesting object identifier. + * @param string $aco ACO The controlled object identifier. + * @param string $action Action (defaults to *) + * @return bool Success + */ + public function allow($aro, $aco, $action = "*"); -/** - * Deny methods are used to remove permission from an ARO to access an ACO. - * - * @param string $aro ARO The requesting object identifier. - * @param string $aco ACO The controlled object identifier. - * @param string $action Action (defaults to *) - * @return bool Success - */ - public function deny($aro, $aco, $action = "*"); + /** + * Deny methods are used to remove permission from an ARO to access an ACO. + * + * @param string $aro ARO The requesting object identifier. + * @param string $aco ACO The controlled object identifier. + * @param string $action Action (defaults to *) + * @return bool Success + */ + public function deny($aro, $aco, $action = "*"); -/** - * Inherit methods modify the permission for an ARO to be that of its parent object. - * - * @param string $aro ARO The requesting object identifier. - * @param string $aco ACO The controlled object identifier. - * @param string $action Action (defaults to *) - * @return bool Success - */ - public function inherit($aro, $aco, $action = "*"); + /** + * Inherit methods modify the permission for an ARO to be that of its parent object. + * + * @param string $aro ARO The requesting object identifier. + * @param string $aco ACO The controlled object identifier. + * @param string $action Action (defaults to *) + * @return bool Success + */ + public function inherit($aro, $aco, $action = "*"); -/** - * Initialization method for the Acl implementation - * - * @param Component $component The AclComponent instance. - * @return void - */ - public function initialize(Component $component); + /** + * Initialization method for the Acl implementation + * + * @param Component $component The AclComponent instance. + * @return void + */ + public function initialize(Component $component); } diff --git a/lib/Cake/Controller/Component/Acl/DbAcl.php b/lib/Cake/Controller/Component/Acl/DbAcl.php index dbbb30fe..e399116b 100755 --- a/lib/Cake/Controller/Component/Acl/DbAcl.php +++ b/lib/Cake/Controller/Component/Acl/DbAcl.php @@ -30,133 +30,144 @@ * Would point to a tree structure like * * ``` - * controllers - * Users - * edit + * controllers + * Users + * edit * ``` * * @package Cake.Controller.Component.Acl */ -class DbAcl extends CakeObject implements AclInterface { +class DbAcl extends CakeObject implements AclInterface +{ -/** - * Constructor - */ - public function __construct() { - parent::__construct(); - $this->Permission = ClassRegistry::init(array('class' => 'Permission', 'alias' => 'Permission')); - $this->Aro = $this->Permission->Aro; - $this->Aco = $this->Permission->Aco; - } + /** + * Constructor + */ + public function __construct() + { + parent::__construct(); + $this->Permission = ClassRegistry::init(['class' => 'Permission', 'alias' => 'Permission']); + $this->Aro = $this->Permission->Aro; + $this->Aco = $this->Permission->Aco; + } -/** - * Initializes the containing component and sets the Aro/Aco objects to it. - * - * @param Component $component The AclComponent instance. - * @return void - */ - public function initialize(Component $component) { - $component->Aro = $this->Aro; - $component->Aco = $this->Aco; - } + /** + * Initializes the containing component and sets the Aro/Aco objects to it. + * + * @param Component $component The AclComponent instance. + * @return void + */ + public function initialize(Component $component) + { + $component->Aro = $this->Aro; + $component->Aco = $this->Aco; + } -/** - * Checks if the given $aro has access to action $action in $aco - * - * @param string $aro ARO The requesting object identifier. - * @param string $aco ACO The controlled object identifier. - * @param string $action Action (defaults to *) - * @return bool Success (true if ARO has access to action in ACO, false otherwise) - * @link https://book.cakephp.org/2.0/en/core-libraries/components/access-control-lists.html#checking-permissions-the-acl-component - */ - public function check($aro, $aco, $action = "*") { - return $this->Permission->check($aro, $aco, $action); - } + /** + * Checks if the given $aro has access to action $action in $aco + * + * @param string $aro ARO The requesting object identifier. + * @param string $aco ACO The controlled object identifier. + * @param string $action Action (defaults to *) + * @return bool Success (true if ARO has access to action in ACO, false otherwise) + * @link https://book.cakephp.org/2.0/en/core-libraries/components/access-control-lists.html#checking-permissions-the-acl-component + */ + public function check($aro, $aco, $action = "*") + { + return $this->Permission->check($aro, $aco, $action); + } -/** - * Allow $aro to have access to action $actions in $aco - * - * @param string $aro ARO The requesting object identifier. - * @param string $aco ACO The controlled object identifier. - * @param string $actions Action (defaults to *) - * @param int $value Value to indicate access type (1 to give access, -1 to deny, 0 to inherit) - * @return bool Success - * @link https://book.cakephp.org/2.0/en/core-libraries/components/access-control-lists.html#assigning-permissions - */ - public function allow($aro, $aco, $actions = "*", $value = 1) { - return $this->Permission->allow($aro, $aco, $actions, $value); - } + /** + * Let access for $aro to action $action in $aco be inherited + * + * @param string $aro ARO The requesting object identifier. + * @param string $aco ACO The controlled object identifier. + * @param string $action Action (defaults to *) + * @return bool Success + */ + public function inherit($aro, $aco, $action = "*") + { + return $this->allow($aro, $aco, $action, 0); + } -/** - * Deny access for $aro to action $action in $aco - * - * @param string $aro ARO The requesting object identifier. - * @param string $aco ACO The controlled object identifier. - * @param string $action Action (defaults to *) - * @return bool Success - * @link https://book.cakephp.org/2.0/en/core-libraries/components/access-control-lists.html#assigning-permissions - */ - public function deny($aro, $aco, $action = "*") { - return $this->allow($aro, $aco, $action, -1); - } + /** + * Allow $aro to have access to action $actions in $aco + * + * @param string $aro ARO The requesting object identifier. + * @param string $aco ACO The controlled object identifier. + * @param string $actions Action (defaults to *) + * @param int $value Value to indicate access type (1 to give access, -1 to deny, 0 to inherit) + * @return bool Success + * @link https://book.cakephp.org/2.0/en/core-libraries/components/access-control-lists.html#assigning-permissions + */ + public function allow($aro, $aco, $actions = "*", $value = 1) + { + return $this->Permission->allow($aro, $aco, $actions, $value); + } -/** - * Let access for $aro to action $action in $aco be inherited - * - * @param string $aro ARO The requesting object identifier. - * @param string $aco ACO The controlled object identifier. - * @param string $action Action (defaults to *) - * @return bool Success - */ - public function inherit($aro, $aco, $action = "*") { - return $this->allow($aro, $aco, $action, 0); - } + /** + * Allow $aro to have access to action $actions in $aco + * + * @param string $aro ARO The requesting object identifier. + * @param string $aco ACO The controlled object identifier. + * @param string $action Action (defaults to *) + * @return bool Success + * @see allow() + */ + public function grant($aro, $aco, $action = "*") + { + return $this->allow($aro, $aco, $action); + } -/** - * Allow $aro to have access to action $actions in $aco - * - * @param string $aro ARO The requesting object identifier. - * @param string $aco ACO The controlled object identifier. - * @param string $action Action (defaults to *) - * @return bool Success - * @see allow() - */ - public function grant($aro, $aco, $action = "*") { - return $this->allow($aro, $aco, $action); - } + /** + * Deny access for $aro to action $action in $aco + * + * @param string $aro ARO The requesting object identifier. + * @param string $aco ACO The controlled object identifier. + * @param string $action Action (defaults to *) + * @return bool Success + * @see deny() + */ + public function revoke($aro, $aco, $action = "*") + { + return $this->deny($aro, $aco, $action); + } -/** - * Deny access for $aro to action $action in $aco - * - * @param string $aro ARO The requesting object identifier. - * @param string $aco ACO The controlled object identifier. - * @param string $action Action (defaults to *) - * @return bool Success - * @see deny() - */ - public function revoke($aro, $aco, $action = "*") { - return $this->deny($aro, $aco, $action); - } + /** + * Deny access for $aro to action $action in $aco + * + * @param string $aro ARO The requesting object identifier. + * @param string $aco ACO The controlled object identifier. + * @param string $action Action (defaults to *) + * @return bool Success + * @link https://book.cakephp.org/2.0/en/core-libraries/components/access-control-lists.html#assigning-permissions + */ + public function deny($aro, $aco, $action = "*") + { + return $this->allow($aro, $aco, $action, -1); + } -/** - * Get an array of access-control links between the given Aro and Aco - * - * @param string $aro ARO The requesting object identifier. - * @param string $aco ACO The controlled object identifier. - * @return array Indexed array with: 'aro', 'aco' and 'link' - */ - public function getAclLink($aro, $aco) { - return $this->Permission->getAclLink($aro, $aco); - } + /** + * Get an array of access-control links between the given Aro and Aco + * + * @param string $aro ARO The requesting object identifier. + * @param string $aco ACO The controlled object identifier. + * @return array Indexed array with: 'aro', 'aco' and 'link' + */ + public function getAclLink($aro, $aco) + { + return $this->Permission->getAclLink($aro, $aco); + } -/** - * Get the keys used in an ACO - * - * @param array $keys Permission model info - * @return array ACO keys - */ - protected function _getAcoKeys($keys) { - return $this->Permission->getAcoKeys($keys); - } + /** + * Get the keys used in an ACO + * + * @param array $keys Permission model info + * @return array ACO keys + */ + protected function _getAcoKeys($keys) + { + return $this->Permission->getAcoKeys($keys); + } } diff --git a/lib/Cake/Controller/Component/Acl/IniAcl.php b/lib/Cake/Controller/Component/Acl/IniAcl.php index 9012cdb4..e86ee046 100755 --- a/lib/Cake/Controller/Component/Acl/IniAcl.php +++ b/lib/Cake/Controller/Component/Acl/IniAcl.php @@ -22,153 +22,161 @@ * * @package Cake.Controller.Component.Acl */ -class IniAcl extends CakeObject implements AclInterface { - -/** - * Array with configuration, parsed from ini file - * - * @var array - */ - public $config = null; - -/** - * The Hash::extract() path to the user/aro identifier in the - * acl.ini file. This path will be used to extract the string - * representation of a user used in the ini file. - * - * @var string - */ - public $userPath = 'User.username'; - -/** - * Initialize method - * - * @param Component $component The AclComponent instance. - * @return void - */ - public function initialize(Component $component) { - } - -/** - * No op method, allow cannot be done with IniAcl - * - * @param string $aro ARO The requesting object identifier. - * @param string $aco ACO The controlled object identifier. - * @param string $action Action (defaults to *) - * @return bool Success - */ - public function allow($aro, $aco, $action = "*") { - } - -/** - * No op method, deny cannot be done with IniAcl - * - * @param string $aro ARO The requesting object identifier. - * @param string $aco ACO The controlled object identifier. - * @param string $action Action (defaults to *) - * @return bool Success - */ - public function deny($aro, $aco, $action = "*") { - } - -/** - * No op method, inherit cannot be done with IniAcl - * - * @param string $aro ARO The requesting object identifier. - * @param string $aco ACO The controlled object identifier. - * @param string $action Action (defaults to *) - * @return bool Success - */ - public function inherit($aro, $aco, $action = "*") { - } - -/** - * Main ACL check function. Checks to see if the ARO (access request object) has access to the - * ACO (access control object).Looks at the acl.ini.php file for permissions - * (see instructions in /config/acl.ini.php). - * - * @param string $aro ARO - * @param string $aco ACO - * @param string $action Action - * @return bool Success - */ - public function check($aro, $aco, $action = null) { - if (!$this->config) { - $this->config = $this->readConfigFile(CONFIG . 'acl.ini.php'); - } - $aclConfig = $this->config; - - if (is_array($aro)) { - $aro = Hash::get($aro, $this->userPath); - } - - if (isset($aclConfig[$aro]['deny'])) { - $userDenies = $this->arrayTrim(explode(",", $aclConfig[$aro]['deny'])); - - if (array_search($aco, $userDenies)) { - return false; - } - } - - if (isset($aclConfig[$aro]['allow'])) { - $userAllows = $this->arrayTrim(explode(",", $aclConfig[$aro]['allow'])); - - if (array_search($aco, $userAllows)) { - return true; - } - } - - if (isset($aclConfig[$aro]['groups'])) { - $userGroups = $this->arrayTrim(explode(",", $aclConfig[$aro]['groups'])); - - foreach ($userGroups as $group) { - if (array_key_exists($group, $aclConfig)) { - if (isset($aclConfig[$group]['deny'])) { - $groupDenies = $this->arrayTrim(explode(",", $aclConfig[$group]['deny'])); - - if (array_search($aco, $groupDenies)) { - return false; - } - } - - if (isset($aclConfig[$group]['allow'])) { - $groupAllows = $this->arrayTrim(explode(",", $aclConfig[$group]['allow'])); - - if (array_search($aco, $groupAllows)) { - return true; - } - } - } - } - } - return false; - } - -/** - * Parses an INI file and returns an array that reflects the - * INI file's section structure. Double-quote friendly. - * - * @param string $filename File - * @return array INI section structure - */ - public function readConfigFile($filename) { - App::uses('IniReader', 'Configure'); - $iniFile = new IniReader(dirname($filename) . DS); - return $iniFile->read(basename($filename)); - } - -/** - * Removes trailing spaces on all array elements (to prepare for searching) - * - * @param array $array Array to trim - * @return array Trimmed array - */ - public function arrayTrim($array) { - foreach ($array as $key => $value) { - $array[$key] = trim($value); - } - array_unshift($array, ""); - return $array; - } +class IniAcl extends CakeObject implements AclInterface +{ + + /** + * Array with configuration, parsed from ini file + * + * @var array + */ + public $config = null; + + /** + * The Hash::extract() path to the user/aro identifier in the + * acl.ini file. This path will be used to extract the string + * representation of a user used in the ini file. + * + * @var string + */ + public $userPath = 'User.username'; + + /** + * Initialize method + * + * @param Component $component The AclComponent instance. + * @return void + */ + public function initialize(Component $component) + { + } + + /** + * No op method, allow cannot be done with IniAcl + * + * @param string $aro ARO The requesting object identifier. + * @param string $aco ACO The controlled object identifier. + * @param string $action Action (defaults to *) + * @return bool Success + */ + public function allow($aro, $aco, $action = "*") + { + } + + /** + * No op method, deny cannot be done with IniAcl + * + * @param string $aro ARO The requesting object identifier. + * @param string $aco ACO The controlled object identifier. + * @param string $action Action (defaults to *) + * @return bool Success + */ + public function deny($aro, $aco, $action = "*") + { + } + + /** + * No op method, inherit cannot be done with IniAcl + * + * @param string $aro ARO The requesting object identifier. + * @param string $aco ACO The controlled object identifier. + * @param string $action Action (defaults to *) + * @return bool Success + */ + public function inherit($aro, $aco, $action = "*") + { + } + + /** + * Main ACL check function. Checks to see if the ARO (access request object) has access to the + * ACO (access control object).Looks at the acl.ini.php file for permissions + * (see instructions in /config/acl.ini.php). + * + * @param string $aro ARO + * @param string $aco ACO + * @param string $action Action + * @return bool Success + */ + public function check($aro, $aco, $action = null) + { + if (!$this->config) { + $this->config = $this->readConfigFile(CONFIG . 'acl.ini.php'); + } + $aclConfig = $this->config; + + if (is_array($aro)) { + $aro = Hash::get($aro, $this->userPath); + } + + if (isset($aclConfig[$aro]['deny'])) { + $userDenies = $this->arrayTrim(explode(",", $aclConfig[$aro]['deny'])); + + if (array_search($aco, $userDenies)) { + return false; + } + } + + if (isset($aclConfig[$aro]['allow'])) { + $userAllows = $this->arrayTrim(explode(",", $aclConfig[$aro]['allow'])); + + if (array_search($aco, $userAllows)) { + return true; + } + } + + if (isset($aclConfig[$aro]['groups'])) { + $userGroups = $this->arrayTrim(explode(",", $aclConfig[$aro]['groups'])); + + foreach ($userGroups as $group) { + if (array_key_exists($group, $aclConfig)) { + if (isset($aclConfig[$group]['deny'])) { + $groupDenies = $this->arrayTrim(explode(",", $aclConfig[$group]['deny'])); + + if (array_search($aco, $groupDenies)) { + return false; + } + } + + if (isset($aclConfig[$group]['allow'])) { + $groupAllows = $this->arrayTrim(explode(",", $aclConfig[$group]['allow'])); + + if (array_search($aco, $groupAllows)) { + return true; + } + } + } + } + } + return false; + } + + /** + * Parses an INI file and returns an array that reflects the + * INI file's section structure. Double-quote friendly. + * + * @param string $filename File + * @return array INI section structure + */ + public function readConfigFile($filename) + { + App::uses('IniReader', 'Configure'); + $iniFile = new IniReader(dirname($filename) . DS); + return $iniFile->read(basename($filename)); + } + + /** + * Removes trailing spaces on all array elements (to prepare for searching) + * + * @param array $array Array to trim + * @return array Trimmed array + */ + public function arrayTrim($array) + { + foreach ($array as $key => $value) { + $array[$key] = trim($value); + } + array_unshift($array, ""); + return $array; + } } diff --git a/lib/Cake/Controller/Component/Acl/PhpAcl.php b/lib/Cake/Controller/Component/Acl/PhpAcl.php index 04176763..ba554862 100755 --- a/lib/Cake/Controller/Component/Acl/PhpAcl.php +++ b/lib/Cake/Controller/Component/Acl/PhpAcl.php @@ -22,540 +22,560 @@ * * @package Cake.Controller.Component.Acl */ -class PhpAcl extends CakeObject implements AclInterface { - -/** - * Constant for deny - * - * @var bool - */ - const DENY = false; - -/** - * Constant for allow - * - * @var bool - */ - const ALLOW = true; - -/** - * Options: - * - policy: determines behavior of the check method. Deny policy needs explicit allow rules, allow policy needs explicit deny rules - * - config: absolute path to config file that contains the acl rules (@see app/Config/acl.php) - * - * @var array - */ - public $options = array(); - -/** - * Aro Object - * - * @var PhpAro - */ - public $Aro = null; - -/** - * Aco Object - * - * @var PhpAco - */ - public $Aco = null; - -/** - * Constructor - * - * Sets a few default settings up. - */ - public function __construct() { - $this->options = array( - 'policy' => static::DENY, - 'config' => CONFIG . 'acl.php', - ); - } - -/** - * Initialize method - * - * @param AclComponent $Component Component instance - * @return void - */ - public function initialize(Component $Component) { - if (!empty($Component->settings['adapter'])) { - $this->options = $Component->settings['adapter'] + $this->options; - } - - App::uses('PhpReader', 'Configure'); - $Reader = new PhpReader(dirname($this->options['config']) . DS); - $config = $Reader->read(basename($this->options['config'])); - $this->build($config); - $Component->Aco = $this->Aco; - $Component->Aro = $this->Aro; - } - -/** - * build and setup internal ACL representation - * - * @param array $config configuration array, see docs - * @return void - * @throws AclException When required keys are missing. - */ - public function build(array $config) { - if (empty($config['roles'])) { - throw new AclException(__d('cake_dev', '"roles" section not found in configuration.')); - } - - if (empty($config['rules']['allow']) && empty($config['rules']['deny'])) { - throw new AclException(__d('cake_dev', 'Neither "allow" nor "deny" rules were provided in configuration.')); - } - - $rules['allow'] = !empty($config['rules']['allow']) ? $config['rules']['allow'] : array(); - $rules['deny'] = !empty($config['rules']['deny']) ? $config['rules']['deny'] : array(); - $roles = !empty($config['roles']) ? $config['roles'] : array(); - $map = !empty($config['map']) ? $config['map'] : array(); - $alias = !empty($config['alias']) ? $config['alias'] : array(); - - $this->Aro = new PhpAro($roles, $map, $alias); - $this->Aco = new PhpAco($rules); - } - -/** - * No op method, allow cannot be done with PhpAcl - * - * @param string $aro ARO The requesting object identifier. - * @param string $aco ACO The controlled object identifier. - * @param string $action Action (defaults to *) - * @return bool Success - */ - public function allow($aro, $aco, $action = "*") { - return $this->Aco->access($this->Aro->resolve($aro), $aco, $action, 'allow'); - } - -/** - * deny ARO access to ACO - * - * @param string $aro ARO The requesting object identifier. - * @param string $aco ACO The controlled object identifier. - * @param string $action Action (defaults to *) - * @return bool Success - */ - public function deny($aro, $aco, $action = "*") { - return $this->Aco->access($this->Aro->resolve($aro), $aco, $action, 'deny'); - } - -/** - * No op method - * - * @param string $aro ARO The requesting object identifier. - * @param string $aco ACO The controlled object identifier. - * @param string $action Action (defaults to *) - * @return bool Success - */ - public function inherit($aro, $aco, $action = "*") { - return false; - } - -/** - * Main ACL check function. Checks to see if the ARO (access request object) has access to the - * ACO (access control object). - * - * @param string $aro ARO - * @param string $aco ACO - * @param string $action Action - * @return bool true if access is granted, false otherwise - */ - public function check($aro, $aco, $action = "*") { - $allow = $this->options['policy']; - $prioritizedAros = $this->Aro->roles($aro); - - if ($action && $action !== "*") { - $aco .= '/' . $action; - } - - $path = $this->Aco->path($aco); - - if (empty($path)) { - return $allow; - } - - foreach ($path as $node) { - foreach ($prioritizedAros as $aros) { - if (!empty($node['allow'])) { - $allow = $allow || count(array_intersect($node['allow'], $aros)); - } - - if (!empty($node['deny'])) { - $allow = $allow && !count(array_intersect($node['deny'], $aros)); - } - } - } - - return $allow; - } +class PhpAcl extends CakeObject implements AclInterface +{ + + /** + * Constant for deny + * + * @var bool + */ + const DENY = false; + + /** + * Constant for allow + * + * @var bool + */ + const ALLOW = true; + + /** + * Options: + * - policy: determines behavior of the check method. Deny policy needs explicit allow rules, allow policy needs explicit deny rules + * - config: absolute path to config file that contains the acl rules (@see app/Config/acl.php) + * + * @var array + */ + public $options = []; + + /** + * Aro Object + * + * @var PhpAro + */ + public $Aro = null; + + /** + * Aco Object + * + * @var PhpAco + */ + public $Aco = null; + + /** + * Constructor + * + * Sets a few default settings up. + */ + public function __construct() + { + $this->options = [ + 'policy' => static::DENY, + 'config' => CONFIG . 'acl.php', + ]; + } + + /** + * Initialize method + * + * @param AclComponent $Component Component instance + * @return void + */ + public function initialize(Component $Component) + { + if (!empty($Component->settings['adapter'])) { + $this->options = $Component->settings['adapter'] + $this->options; + } + + App::uses('PhpReader', 'Configure'); + $Reader = new PhpReader(dirname($this->options['config']) . DS); + $config = $Reader->read(basename($this->options['config'])); + $this->build($config); + $Component->Aco = $this->Aco; + $Component->Aro = $this->Aro; + } + + /** + * build and setup internal ACL representation + * + * @param array $config configuration array, see docs + * @return void + * @throws AclException When required keys are missing. + */ + public function build(array $config) + { + if (empty($config['roles'])) { + throw new AclException(__d('cake_dev', '"roles" section not found in configuration.')); + } + + if (empty($config['rules']['allow']) && empty($config['rules']['deny'])) { + throw new AclException(__d('cake_dev', 'Neither "allow" nor "deny" rules were provided in configuration.')); + } + + $rules['allow'] = !empty($config['rules']['allow']) ? $config['rules']['allow'] : []; + $rules['deny'] = !empty($config['rules']['deny']) ? $config['rules']['deny'] : []; + $roles = !empty($config['roles']) ? $config['roles'] : []; + $map = !empty($config['map']) ? $config['map'] : []; + $alias = !empty($config['alias']) ? $config['alias'] : []; + + $this->Aro = new PhpAro($roles, $map, $alias); + $this->Aco = new PhpAco($rules); + } + + /** + * No op method, allow cannot be done with PhpAcl + * + * @param string $aro ARO The requesting object identifier. + * @param string $aco ACO The controlled object identifier. + * @param string $action Action (defaults to *) + * @return bool Success + */ + public function allow($aro, $aco, $action = "*") + { + return $this->Aco->access($this->Aro->resolve($aro), $aco, $action, 'allow'); + } + + /** + * deny ARO access to ACO + * + * @param string $aro ARO The requesting object identifier. + * @param string $aco ACO The controlled object identifier. + * @param string $action Action (defaults to *) + * @return bool Success + */ + public function deny($aro, $aco, $action = "*") + { + return $this->Aco->access($this->Aro->resolve($aro), $aco, $action, 'deny'); + } + + /** + * No op method + * + * @param string $aro ARO The requesting object identifier. + * @param string $aco ACO The controlled object identifier. + * @param string $action Action (defaults to *) + * @return bool Success + */ + public function inherit($aro, $aco, $action = "*") + { + return false; + } + + /** + * Main ACL check function. Checks to see if the ARO (access request object) has access to the + * ACO (access control object). + * + * @param string $aro ARO + * @param string $aco ACO + * @param string $action Action + * @return bool true if access is granted, false otherwise + */ + public function check($aro, $aco, $action = "*") + { + $allow = $this->options['policy']; + $prioritizedAros = $this->Aro->roles($aro); + + if ($action && $action !== "*") { + $aco .= '/' . $action; + } + + $path = $this->Aco->path($aco); + + if (empty($path)) { + return $allow; + } + + foreach ($path as $node) { + foreach ($prioritizedAros as $aros) { + if (!empty($node['allow'])) { + $allow = $allow || count(array_intersect($node['allow'], $aros)); + } + + if (!empty($node['deny'])) { + $allow = $allow && !count(array_intersect($node['deny'], $aros)); + } + } + } + + return $allow; + } } /** * Access Control Object */ -class PhpAco { - -/** - * holds internal ACO representation - * - * @var array - */ - protected $_tree = array(); - -/** - * map modifiers for ACO paths to their respective PCRE pattern - * - * @var array - */ - public static $modifiers = array( - '*' => '.*', - ); - -/** - * Constructor - * - * @param array $rules Rules array - */ - public function __construct(array $rules = array()) { - foreach (array('allow', 'deny') as $type) { - if (empty($rules[$type])) { - $rules[$type] = array(); - } - } - - $this->build($rules['allow'], $rules['deny']); - } - -/** - * return path to the requested ACO with allow and deny rules attached on each level - * - * @param string $aco ACO string - * @return array - */ - public function path($aco) { - $aco = $this->resolve($aco); - $path = array(); - $level = 0; - $root = $this->_tree; - $stack = array(array($root, 0)); - - while (!empty($stack)) { - list($root, $level) = array_pop($stack); - - if (empty($path[$level])) { - $path[$level] = array(); - } - - foreach ($root as $node => $elements) { - $pattern = '/^' . str_replace(array_keys(static::$modifiers), array_values(static::$modifiers), $node) . '$/'; - - if ($node == $aco[$level] || preg_match($pattern, $aco[$level])) { - // merge allow/denies with $path of current level - foreach (array('allow', 'deny') as $policy) { - if (!empty($elements[$policy])) { - if (empty($path[$level][$policy])) { - $path[$level][$policy] = array(); - } - $path[$level][$policy] = array_merge($path[$level][$policy], $elements[$policy]); - } - } - - // traverse - if (!empty($elements['children']) && isset($aco[$level + 1])) { - array_push($stack, array($elements['children'], $level + 1)); - } - } - } - } - - return $path; - } - -/** - * allow/deny ARO access to ARO - * - * @param string $aro ARO string - * @param string $aco ACO string - * @param string $action Action string - * @param string $type access type - * @return void - */ - public function access($aro, $aco, $action, $type = 'deny') { - $aco = $this->resolve($aco); - $depth = count($aco); - $root = $this->_tree; - $tree = &$root; - - foreach ($aco as $i => $node) { - if (!isset($tree[$node])) { - $tree[$node] = array( - 'children' => array(), - ); - } - - if ($i < $depth - 1) { - $tree = &$tree[$node]['children']; - } else { - if (empty($tree[$node][$type])) { - $tree[$node][$type] = array(); - } - - $tree[$node][$type] = array_merge(is_array($aro) ? $aro : array($aro), $tree[$node][$type]); - } - } - - $this->_tree = &$root; - } - -/** - * resolve given ACO string to a path - * - * @param string $aco ACO string - * @return array path - */ - public function resolve($aco) { - if (is_array($aco)) { - return array_map('strtolower', $aco); - } - - // strip multiple occurrences of '/' - $aco = preg_replace('#/+#', '/', $aco); - // make case insensitive - $aco = ltrim(strtolower($aco), '/'); - return array_filter(array_map('trim', explode('/', $aco))); - } - -/** - * build a tree representation from the given allow/deny informations for ACO paths - * - * @param array $allow ACO allow rules - * @param array $deny ACO deny rules - * @return void - */ - public function build(array $allow, array $deny = array()) { - $this->_tree = array(); - - foreach ($allow as $dotPath => $aros) { - if (is_string($aros)) { - $aros = array_map('trim', explode(',', $aros)); - } - - $this->access($aros, $dotPath, null, 'allow'); - } - - foreach ($deny as $dotPath => $aros) { - if (is_string($aros)) { - $aros = array_map('trim', explode(',', $aros)); - } - - $this->access($aros, $dotPath, null, 'deny'); - } - } +class PhpAco +{ + + /** + * map modifiers for ACO paths to their respective PCRE pattern + * + * @var array + */ + public static $modifiers = [ + '*' => '.*', + ]; + /** + * holds internal ACO representation + * + * @var array + */ + protected $_tree = []; + + /** + * Constructor + * + * @param array $rules Rules array + */ + public function __construct(array $rules = []) + { + foreach (['allow', 'deny'] as $type) { + if (empty($rules[$type])) { + $rules[$type] = []; + } + } + + $this->build($rules['allow'], $rules['deny']); + } + + /** + * build a tree representation from the given allow/deny informations for ACO paths + * + * @param array $allow ACO allow rules + * @param array $deny ACO deny rules + * @return void + */ + public function build(array $allow, array $deny = []) + { + $this->_tree = []; + + foreach ($allow as $dotPath => $aros) { + if (is_string($aros)) { + $aros = array_map('trim', explode(',', $aros)); + } + + $this->access($aros, $dotPath, null, 'allow'); + } + + foreach ($deny as $dotPath => $aros) { + if (is_string($aros)) { + $aros = array_map('trim', explode(',', $aros)); + } + + $this->access($aros, $dotPath, null, 'deny'); + } + } + + /** + * allow/deny ARO access to ARO + * + * @param string $aro ARO string + * @param string $aco ACO string + * @param string $action Action string + * @param string $type access type + * @return void + */ + public function access($aro, $aco, $action, $type = 'deny') + { + $aco = $this->resolve($aco); + $depth = count($aco); + $root = $this->_tree; + $tree = &$root; + + foreach ($aco as $i => $node) { + if (!isset($tree[$node])) { + $tree[$node] = [ + 'children' => [], + ]; + } + + if ($i < $depth - 1) { + $tree = &$tree[$node]['children']; + } else { + if (empty($tree[$node][$type])) { + $tree[$node][$type] = []; + } + + $tree[$node][$type] = array_merge(is_array($aro) ? $aro : [$aro], $tree[$node][$type]); + } + } + + $this->_tree = &$root; + } + + /** + * resolve given ACO string to a path + * + * @param string $aco ACO string + * @return array path + */ + public function resolve($aco) + { + if (is_array($aco)) { + return array_map('strtolower', $aco); + } + + // strip multiple occurrences of '/' + $aco = preg_replace('#/+#', '/', $aco); + // make case insensitive + $aco = ltrim(strtolower($aco), '/'); + return array_filter(array_map('trim', explode('/', $aco))); + } + + /** + * return path to the requested ACO with allow and deny rules attached on each level + * + * @param string $aco ACO string + * @return array + */ + public function path($aco) + { + $aco = $this->resolve($aco); + $path = []; + $level = 0; + $root = $this->_tree; + $stack = [[$root, 0]]; + + while (!empty($stack)) { + list($root, $level) = array_pop($stack); + + if (empty($path[$level])) { + $path[$level] = []; + } + + foreach ($root as $node => $elements) { + $pattern = '/^' . str_replace(array_keys(static::$modifiers), array_values(static::$modifiers), $node) . '$/'; + + if ($node == $aco[$level] || preg_match($pattern, $aco[$level])) { + // merge allow/denies with $path of current level + foreach (['allow', 'deny'] as $policy) { + if (!empty($elements[$policy])) { + if (empty($path[$level][$policy])) { + $path[$level][$policy] = []; + } + $path[$level][$policy] = array_merge($path[$level][$policy], $elements[$policy]); + } + } + + // traverse + if (!empty($elements['children']) && isset($aco[$level + 1])) { + array_push($stack, [$elements['children'], $level + 1]); + } + } + } + } + + return $path; + } } /** * Access Request Object */ -class PhpAro { - -/** - * role to resolve to when a provided ARO is not listed in - * the internal tree - * - * @var string - */ - const DEFAULT_ROLE = 'Role/default'; - -/** - * map external identifiers. E.g. if - * - * array('User' => array('username' => 'jeff', 'role' => 'editor')) - * - * is passed as an ARO to one of the methods of AclComponent, PhpAcl - * will check if it can be resolved to an User or a Role defined in the - * configuration file. - * - * @var array - * @see app/Config/acl.php - */ - public $map = array( - 'User' => 'User/username', - 'Role' => 'User/role', - ); - -/** - * aliases to map - * - * @var array - */ - public $aliases = array(); - -/** - * internal ARO representation - * - * @var array - */ - protected $_tree = array(); - -/** - * Constructor - * - * @param array $aro The aro data - * @param array $map The identifier mappings - * @param array $aliases The aliases to map. - */ - public function __construct(array $aro = array(), array $map = array(), array $aliases = array()) { - if (!empty($map)) { - $this->map = $map; - } - - $this->aliases = $aliases; - $this->build($aro); - } - -/** - * From the perspective of the given ARO, walk down the tree and - * collect all inherited AROs levelwise such that AROs from different - * branches with equal distance to the requested ARO will be collected at the same - * index. The resulting array will contain a prioritized list of (list of) roles ordered from - * the most distant AROs to the requested one itself. - * - * @param string|array $aro An ARO identifier - * @return array prioritized AROs - */ - public function roles($aro) { - $aros = array(); - $aro = $this->resolve($aro); - $stack = array(array($aro, 0)); - - while (!empty($stack)) { - list($element, $depth) = array_pop($stack); - $aros[$depth][] = $element; - - foreach ($this->_tree as $node => $children) { - if (in_array($element, $children)) { - array_push($stack, array($node, $depth + 1)); - } - } - } - - return array_reverse($aros); - } - -/** - * resolve an ARO identifier to an internal ARO string using - * the internal mapping information. - * - * @param string|array $aro ARO identifier (User.jeff, array('User' => ...), etc) - * @return string internal aro string (e.g. User/jeff, Role/default) - */ - public function resolve($aro) { - foreach ($this->map as $aroGroup => $map) { - list ($model, $field) = explode('/', $map, 2); - $mapped = ''; - - if (is_array($aro)) { - if (isset($aro['model']) && isset($aro['foreign_key']) && $aro['model'] === $aroGroup) { - $mapped = $aroGroup . '/' . $aro['foreign_key']; - } elseif (isset($aro[$model][$field])) { - $mapped = $aroGroup . '/' . $aro[$model][$field]; - } elseif (isset($aro[$field])) { - $mapped = $aroGroup . '/' . $aro[$field]; - } - } elseif (is_string($aro)) { - $aro = ltrim($aro, '/'); - - if (strpos($aro, '/') === false) { - $mapped = $aroGroup . '/' . $aro; - } else { - list($aroModel, $aroValue) = explode('/', $aro, 2); - - $aroModel = Inflector::camelize($aroModel); - - if ($aroModel === $model || $aroModel === $aroGroup) { - $mapped = $aroGroup . '/' . $aroValue; - } - } - } - - if (isset($this->_tree[$mapped])) { - return $mapped; - } - - // is there a matching alias defined (e.g. Role/1 => Role/admin)? - if (!empty($this->aliases[$mapped])) { - return $this->aliases[$mapped]; - } - } - return static::DEFAULT_ROLE; - } - -/** - * adds a new ARO to the tree - * - * @param array $aro one or more ARO records - * @return void - */ - public function addRole(array $aro) { - foreach ($aro as $role => $inheritedRoles) { - if (!isset($this->_tree[$role])) { - $this->_tree[$role] = array(); - } - - if (!empty($inheritedRoles)) { - if (is_string($inheritedRoles)) { - $inheritedRoles = array_map('trim', explode(',', $inheritedRoles)); - } - - foreach ($inheritedRoles as $dependency) { - // detect cycles - $roles = $this->roles($dependency); - - if (in_array($role, Hash::flatten($roles))) { - $path = ''; - - foreach ($roles as $roleDependencies) { - $path .= implode('|', (array)$roleDependencies) . ' -> '; - } - - trigger_error(__d('cake_dev', 'cycle detected when inheriting %s from %s. Path: %s', $role, $dependency, $path . $role)); - continue; - } - - if (!isset($this->_tree[$dependency])) { - $this->_tree[$dependency] = array(); - } - - $this->_tree[$dependency][] = $role; - } - } - } - } - -/** - * adds one or more aliases to the internal map. Overwrites existing entries. - * - * @param array $alias alias from => to (e.g. Role/13 -> Role/editor) - * @return void - */ - public function addAlias(array $alias) { - $this->aliases = $alias + $this->aliases; - } - -/** - * build an ARO tree structure for internal processing - * - * @param array $aros array of AROs as key and their inherited AROs as values - * @return void - */ - public function build(array $aros) { - $this->_tree = array(); - $this->addRole($aros); - } +class PhpAro +{ + + /** + * role to resolve to when a provided ARO is not listed in + * the internal tree + * + * @var string + */ + const DEFAULT_ROLE = 'Role/default'; + + /** + * map external identifiers. E.g. if + * + * array('User' => array('username' => 'jeff', 'role' => 'editor')) + * + * is passed as an ARO to one of the methods of AclComponent, PhpAcl + * will check if it can be resolved to an User or a Role defined in the + * configuration file. + * + * @var array + * @see app/Config/acl.php + */ + public $map = [ + 'User' => 'User/username', + 'Role' => 'User/role', + ]; + + /** + * aliases to map + * + * @var array + */ + public $aliases = []; + + /** + * internal ARO representation + * + * @var array + */ + protected $_tree = []; + + /** + * Constructor + * + * @param array $aro The aro data + * @param array $map The identifier mappings + * @param array $aliases The aliases to map. + */ + public function __construct(array $aro = [], array $map = [], array $aliases = []) + { + if (!empty($map)) { + $this->map = $map; + } + + $this->aliases = $aliases; + $this->build($aro); + } + + /** + * build an ARO tree structure for internal processing + * + * @param array $aros array of AROs as key and their inherited AROs as values + * @return void + */ + public function build(array $aros) + { + $this->_tree = []; + $this->addRole($aros); + } + + /** + * adds a new ARO to the tree + * + * @param array $aro one or more ARO records + * @return void + */ + public function addRole(array $aro) + { + foreach ($aro as $role => $inheritedRoles) { + if (!isset($this->_tree[$role])) { + $this->_tree[$role] = []; + } + + if (!empty($inheritedRoles)) { + if (is_string($inheritedRoles)) { + $inheritedRoles = array_map('trim', explode(',', $inheritedRoles)); + } + + foreach ($inheritedRoles as $dependency) { + // detect cycles + $roles = $this->roles($dependency); + + if (in_array($role, Hash::flatten($roles))) { + $path = ''; + + foreach ($roles as $roleDependencies) { + $path .= implode('|', (array)$roleDependencies) . ' -> '; + } + + trigger_error(__d('cake_dev', 'cycle detected when inheriting %s from %s. Path: %s', $role, $dependency, $path . $role)); + continue; + } + + if (!isset($this->_tree[$dependency])) { + $this->_tree[$dependency] = []; + } + + $this->_tree[$dependency][] = $role; + } + } + } + } + + /** + * From the perspective of the given ARO, walk down the tree and + * collect all inherited AROs levelwise such that AROs from different + * branches with equal distance to the requested ARO will be collected at the same + * index. The resulting array will contain a prioritized list of (list of) roles ordered from + * the most distant AROs to the requested one itself. + * + * @param string|array $aro An ARO identifier + * @return array prioritized AROs + */ + public function roles($aro) + { + $aros = []; + $aro = $this->resolve($aro); + $stack = [[$aro, 0]]; + + while (!empty($stack)) { + list($element, $depth) = array_pop($stack); + $aros[$depth][] = $element; + + foreach ($this->_tree as $node => $children) { + if (in_array($element, $children)) { + array_push($stack, [$node, $depth + 1]); + } + } + } + + return array_reverse($aros); + } + + /** + * resolve an ARO identifier to an internal ARO string using + * the internal mapping information. + * + * @param string|array $aro ARO identifier (User.jeff, array('User' => ...), etc) + * @return string internal aro string (e.g. User/jeff, Role/default) + */ + public function resolve($aro) + { + foreach ($this->map as $aroGroup => $map) { + list ($model, $field) = explode('/', $map, 2); + $mapped = ''; + + if (is_array($aro)) { + if (isset($aro['model']) && isset($aro['foreign_key']) && $aro['model'] === $aroGroup) { + $mapped = $aroGroup . '/' . $aro['foreign_key']; + } else if (isset($aro[$model][$field])) { + $mapped = $aroGroup . '/' . $aro[$model][$field]; + } else if (isset($aro[$field])) { + $mapped = $aroGroup . '/' . $aro[$field]; + } + } else if (is_string($aro)) { + $aro = ltrim($aro, '/'); + + if (strpos($aro, '/') === false) { + $mapped = $aroGroup . '/' . $aro; + } else { + list($aroModel, $aroValue) = explode('/', $aro, 2); + + $aroModel = Inflector::camelize($aroModel); + + if ($aroModel === $model || $aroModel === $aroGroup) { + $mapped = $aroGroup . '/' . $aroValue; + } + } + } + + if (isset($this->_tree[$mapped])) { + return $mapped; + } + + // is there a matching alias defined (e.g. Role/1 => Role/admin)? + if (!empty($this->aliases[$mapped])) { + return $this->aliases[$mapped]; + } + } + return static::DEFAULT_ROLE; + } + + /** + * adds one or more aliases to the internal map. Overwrites existing entries. + * + * @param array $alias alias from => to (e.g. Role/13 -> Role/editor) + * @return void + */ + public function addAlias(array $alias) + { + $this->aliases = $alias + $this->aliases; + } } diff --git a/lib/Cake/Controller/Component/AclComponent.php b/lib/Cake/Controller/Component/AclComponent.php index c6f3ec79..ea033139 100755 --- a/lib/Cake/Controller/Component/AclComponent.php +++ b/lib/Cake/Controller/Component/AclComponent.php @@ -27,154 +27,161 @@ * @package Cake.Controller.Component * @link https://book.cakephp.org/2.0/en/core-libraries/components/access-control-lists.html */ -class AclComponent extends Component { +class AclComponent extends Component +{ -/** - * Instance of an ACL class - * - * @var AclInterface - */ - protected $_Instance = null; + /** + * Aro object. + * + * @var string + */ + public $Aro; + /** + * Aco object + * + * @var string + */ + public $Aco; + /** + * Instance of an ACL class + * + * @var AclInterface + */ + protected $_Instance = null; -/** - * Aro object. - * - * @var string - */ - public $Aro; + /** + * Constructor. Will return an instance of the correct ACL class as defined in `Configure::read('Acl.classname')` + * + * @param ComponentCollection $collection Collection instance. + * @param array $settings Settings list. + * @throws CakeException when Acl.classname could not be loaded. + */ + public function __construct(ComponentCollection $collection, $settings = []) + { + parent::__construct($collection, $settings); + $name = Configure::read('Acl.classname'); + if (!class_exists($name)) { + list($plugin, $name) = pluginSplit($name, true); + App::uses($name, $plugin . 'Controller/Component/Acl'); + if (!class_exists($name)) { + throw new CakeException(__d('cake_dev', 'Could not find %s.', $name)); + } + } + $this->adapter($name); + } -/** - * Aco object - * - * @var string - */ - public $Aco; + /** + * Sets or gets the Adapter object currently in the AclComponent. + * + * `$this->Acl->adapter();` will get the current adapter class while + * `$this->Acl->adapter($obj);` will set the adapter class + * + * Will call the initialize method on the adapter if setting a new one. + * + * @param AclInterface|string $adapter Instance of AclInterface or a string name of the class to use. (optional) + * @return AclInterface|null Either null, or the adapter implementation. + * @throws CakeException when the given class is not an instance of AclInterface + */ + public function adapter($adapter = null) + { + if ($adapter) { + if (is_string($adapter)) { + $adapter = new $adapter(); + } + if (!$adapter instanceof AclInterface) { + throw new CakeException(__d('cake_dev', 'AclComponent adapters must implement AclInterface')); + } + $this->_Instance = $adapter; + $this->_Instance->initialize($this); + return null; + } + return $this->_Instance; + } -/** - * Constructor. Will return an instance of the correct ACL class as defined in `Configure::read('Acl.classname')` - * - * @param ComponentCollection $collection Collection instance. - * @param array $settings Settings list. - * @throws CakeException when Acl.classname could not be loaded. - */ - public function __construct(ComponentCollection $collection, $settings = array()) { - parent::__construct($collection, $settings); - $name = Configure::read('Acl.classname'); - if (!class_exists($name)) { - list($plugin, $name) = pluginSplit($name, true); - App::uses($name, $plugin . 'Controller/Component/Acl'); - if (!class_exists($name)) { - throw new CakeException(__d('cake_dev', 'Could not find %s.', $name)); - } - } - $this->adapter($name); - } + /** + * Pass-thru function for ACL check instance. Check methods + * are used to check whether or not an ARO can access an ACO + * + * @param array|string|Model $aro ARO The requesting object identifier. See `AclNode::node()` for possible formats + * @param array|string|Model $aco ACO The controlled object identifier. See `AclNode::node()` for possible formats + * @param string $action Action (defaults to *) + * @return bool Success + */ + public function check($aro, $aco, $action = "*") + { + return $this->_Instance->check($aro, $aco, $action); + } -/** - * Sets or gets the Adapter object currently in the AclComponent. - * - * `$this->Acl->adapter();` will get the current adapter class while - * `$this->Acl->adapter($obj);` will set the adapter class - * - * Will call the initialize method on the adapter if setting a new one. - * - * @param AclInterface|string $adapter Instance of AclInterface or a string name of the class to use. (optional) - * @return AclInterface|null Either null, or the adapter implementation. - * @throws CakeException when the given class is not an instance of AclInterface - */ - public function adapter($adapter = null) { - if ($adapter) { - if (is_string($adapter)) { - $adapter = new $adapter(); - } - if (!$adapter instanceof AclInterface) { - throw new CakeException(__d('cake_dev', 'AclComponent adapters must implement AclInterface')); - } - $this->_Instance = $adapter; - $this->_Instance->initialize($this); - return null; - } - return $this->_Instance; - } + /** + * Pass-thru function for ACL allow instance. Allow methods + * are used to grant an ARO access to an ACO. + * + * @param array|string|Model $aro ARO The requesting object identifier. See `AclNode::node()` for possible formats + * @param array|string|Model $aco ACO The controlled object identifier. See `AclNode::node()` for possible formats + * @param string $action Action (defaults to *) + * @return bool Success + */ + public function allow($aro, $aco, $action = "*") + { + return $this->_Instance->allow($aro, $aco, $action); + } -/** - * Pass-thru function for ACL check instance. Check methods - * are used to check whether or not an ARO can access an ACO - * - * @param array|string|Model $aro ARO The requesting object identifier. See `AclNode::node()` for possible formats - * @param array|string|Model $aco ACO The controlled object identifier. See `AclNode::node()` for possible formats - * @param string $action Action (defaults to *) - * @return bool Success - */ - public function check($aro, $aco, $action = "*") { - return $this->_Instance->check($aro, $aco, $action); - } - -/** - * Pass-thru function for ACL allow instance. Allow methods - * are used to grant an ARO access to an ACO. - * - * @param array|string|Model $aro ARO The requesting object identifier. See `AclNode::node()` for possible formats - * @param array|string|Model $aco ACO The controlled object identifier. See `AclNode::node()` for possible formats - * @param string $action Action (defaults to *) - * @return bool Success - */ - public function allow($aro, $aco, $action = "*") { - return $this->_Instance->allow($aro, $aco, $action); - } - -/** - * Pass-thru function for ACL deny instance. Deny methods - * are used to remove permission from an ARO to access an ACO. - * - * @param array|string|Model $aro ARO The requesting object identifier. See `AclNode::node()` for possible formats - * @param array|string|Model $aco ACO The controlled object identifier. See `AclNode::node()` for possible formats - * @param string $action Action (defaults to *) - * @return bool Success - */ - public function deny($aro, $aco, $action = "*") { - return $this->_Instance->deny($aro, $aco, $action); - } + /** + * Pass-thru function for ACL deny instance. Deny methods + * are used to remove permission from an ARO to access an ACO. + * + * @param array|string|Model $aro ARO The requesting object identifier. See `AclNode::node()` for possible formats + * @param array|string|Model $aco ACO The controlled object identifier. See `AclNode::node()` for possible formats + * @param string $action Action (defaults to *) + * @return bool Success + */ + public function deny($aro, $aco, $action = "*") + { + return $this->_Instance->deny($aro, $aco, $action); + } -/** - * Pass-thru function for ACL inherit instance. Inherit methods - * modify the permission for an ARO to be that of its parent object. - * - * @param array|string|Model $aro ARO The requesting object identifier. See `AclNode::node()` for possible formats - * @param array|string|Model $aco ACO The controlled object identifier. See `AclNode::node()` for possible formats - * @param string $action Action (defaults to *) - * @return bool Success - */ - public function inherit($aro, $aco, $action = "*") { - return $this->_Instance->inherit($aro, $aco, $action); - } + /** + * Pass-thru function for ACL inherit instance. Inherit methods + * modify the permission for an ARO to be that of its parent object. + * + * @param array|string|Model $aro ARO The requesting object identifier. See `AclNode::node()` for possible formats + * @param array|string|Model $aco ACO The controlled object identifier. See `AclNode::node()` for possible formats + * @param string $action Action (defaults to *) + * @return bool Success + */ + public function inherit($aro, $aco, $action = "*") + { + return $this->_Instance->inherit($aro, $aco, $action); + } -/** - * Pass-thru function for ACL grant instance. An alias for AclComponent::allow() - * - * @param array|string|Model $aro ARO The requesting object identifier. See `AclNode::node()` for possible formats - * @param array|string|Model $aco ACO The controlled object identifier. See `AclNode::node()` for possible formats - * @param string $action Action (defaults to *) - * @return bool Success - * @deprecated 3.0.0 Will be removed in 3.0. - */ - public function grant($aro, $aco, $action = "*") { - trigger_error(__d('cake_dev', '%s is deprecated, use %s instead', 'AclComponent::grant()', 'allow()'), E_USER_WARNING); - return $this->_Instance->allow($aro, $aco, $action); - } + /** + * Pass-thru function for ACL grant instance. An alias for AclComponent::allow() + * + * @param array|string|Model $aro ARO The requesting object identifier. See `AclNode::node()` for possible formats + * @param array|string|Model $aco ACO The controlled object identifier. See `AclNode::node()` for possible formats + * @param string $action Action (defaults to *) + * @return bool Success + * @deprecated 3.0.0 Will be removed in 3.0. + */ + public function grant($aro, $aco, $action = "*") + { + trigger_error(__d('cake_dev', '%s is deprecated, use %s instead', 'AclComponent::grant()', 'allow()'), E_USER_WARNING); + return $this->_Instance->allow($aro, $aco, $action); + } -/** - * Pass-thru function for ACL grant instance. An alias for AclComponent::deny() - * - * @param array|string|Model $aro ARO The requesting object identifier. See `AclNode::node()` for possible formats - * @param array|string|Model $aco ACO The controlled object identifier. See `AclNode::node()` for possible formats - * @param string $action Action (defaults to *) - * @return bool Success - * @deprecated 3.0.0 Will be removed in 3.0. - */ - public function revoke($aro, $aco, $action = "*") { - trigger_error(__d('cake_dev', '%s is deprecated, use %s instead', 'AclComponent::revoke()', 'deny()'), E_USER_WARNING); - return $this->_Instance->deny($aro, $aco, $action); - } + /** + * Pass-thru function for ACL grant instance. An alias for AclComponent::deny() + * + * @param array|string|Model $aro ARO The requesting object identifier. See `AclNode::node()` for possible formats + * @param array|string|Model $aco ACO The controlled object identifier. See `AclNode::node()` for possible formats + * @param string $action Action (defaults to *) + * @return bool Success + * @deprecated 3.0.0 Will be removed in 3.0. + */ + public function revoke($aro, $aco, $action = "*") + { + trigger_error(__d('cake_dev', '%s is deprecated, use %s instead', 'AclComponent::revoke()', 'deny()'), E_USER_WARNING); + return $this->_Instance->deny($aro, $aco, $action); + } } diff --git a/lib/Cake/Controller/Component/Auth/AbstractPasswordHasher.php b/lib/Cake/Controller/Component/Auth/AbstractPasswordHasher.php index ddabbfb8..236dca3e 100755 --- a/lib/Cake/Controller/Component/Auth/AbstractPasswordHasher.php +++ b/lib/Cake/Controller/Component/Auth/AbstractPasswordHasher.php @@ -18,55 +18,58 @@ * * @package Cake.Controller.Component.Auth */ -abstract class AbstractPasswordHasher { +abstract class AbstractPasswordHasher +{ -/** - * Configurations for this object. Settings passed from authenticator class to - * the constructor are merged with this property. - * - * @var array - */ - protected $_config = array(); + /** + * Configurations for this object. Settings passed from authenticator class to + * the constructor are merged with this property. + * + * @var array + */ + protected $_config = []; -/** - * Constructor - * - * @param array $config Array of config. - */ - public function __construct($config = array()) { - $this->config($config); - } + /** + * Constructor + * + * @param array $config Array of config. + */ + public function __construct($config = []) + { + $this->config($config); + } -/** - * Get/Set the config - * - * @param array $config Sets config, if null returns existing config - * @return array Returns configs - */ - public function config($config = null) { - if (is_array($config)) { - $this->_config = array_merge($this->_config, $config); - } - return $this->_config; - } + /** + * Get/Set the config + * + * @param array $config Sets config, if null returns existing config + * @return array Returns configs + */ + public function config($config = null) + { + if (is_array($config)) { + $this->_config = array_merge($this->_config, $config); + } + return $this->_config; + } -/** - * Generates password hash. - * - * @param string|array $password Plain text password to hash or array of data - * required to generate password hash. - * @return string Password hash - */ - abstract public function hash($password); + /** + * Generates password hash. + * + * @param string|array $password Plain text password to hash or array of data + * required to generate password hash. + * @return string Password hash + */ + abstract public function hash($password); -/** - * Check hash. Generate hash from user provided password string or data array - * and check against existing hash. - * - * @param string|array $password Plain text password to hash or data array. - * @param string $hashedPassword Existing hashed password. - * @return bool True if hashes match else false. - */ - abstract public function check($password, $hashedPassword); + /** + * Check hash. Generate hash from user provided password string or data array + * and check against existing hash. + * + * @param string|array $password Plain text password to hash or data array. + * @param string $hashedPassword Existing hashed password. + * @return bool True if hashes match else false. + */ + abstract public function check($password, $hashedPassword); } diff --git a/lib/Cake/Controller/Component/Auth/ActionsAuthorize.php b/lib/Cake/Controller/Component/Auth/ActionsAuthorize.php index 965e5fbd..fe2e8daa 100755 --- a/lib/Cake/Controller/Component/Auth/ActionsAuthorize.php +++ b/lib/Cake/Controller/Component/Auth/ActionsAuthorize.php @@ -23,19 +23,21 @@ * @see AuthComponent::$authenticate * @see AclComponent::check() */ -class ActionsAuthorize extends BaseAuthorize { +class ActionsAuthorize extends BaseAuthorize +{ -/** - * Authorize a user using the AclComponent. - * - * @param array $user The user to authorize - * @param CakeRequest $request The request needing authorization. - * @return bool - */ - public function authorize($user, CakeRequest $request) { - $Acl = $this->_Collection->load('Acl'); - $user = array($this->settings['userModel'] => $user); - return $Acl->check($user, $this->action($request)); - } + /** + * Authorize a user using the AclComponent. + * + * @param array $user The user to authorize + * @param CakeRequest $request The request needing authorization. + * @return bool + */ + public function authorize($user, CakeRequest $request) + { + $Acl = $this->_Collection->load('Acl'); + $user = [$this->settings['userModel'] => $user]; + return $Acl->check($user, $this->action($request)); + } } diff --git a/lib/Cake/Controller/Component/Auth/BaseAuthenticate.php b/lib/Cake/Controller/Component/Auth/BaseAuthenticate.php index 1c7e59e3..6f91a299 100755 --- a/lib/Cake/Controller/Component/Auth/BaseAuthenticate.php +++ b/lib/Cake/Controller/Component/Auth/BaseAuthenticate.php @@ -21,217 +21,226 @@ * * @package Cake.Controller.Component.Auth */ -abstract class BaseAuthenticate implements CakeEventListener { - -/** - * Settings for this object. - * - * - `fields` The fields to use to identify a user by. - * - `userModel` The model name of the User, defaults to User. - * - `userFields` Array of fields to retrieve from User model, null to retrieve all. Defaults to null. - * - `scope` Additional conditions to use when looking up and authenticating users, - * i.e. `array('User.is_active' => 1).` - * - `recursive` The value of the recursive key passed to find(). Defaults to 0. - * - `contain` Extra models to contain and store in session. - * - `passwordHasher` Password hasher class. Can be a string specifying class name - * or an array containing `className` key, any other keys will be passed as - * settings to the class. Defaults to 'Simple'. - * - * @var array - */ - public $settings = array( - 'fields' => array( - 'username' => 'username', - 'password' => 'password' - ), - 'userModel' => 'User', - 'userFields' => null, - 'scope' => array(), - 'recursive' => 0, - 'contain' => null, - 'passwordHasher' => 'Simple' - ); - -/** - * A Component collection, used to get more components. - * - * @var ComponentCollection - */ - protected $_Collection; - -/** - * Password hasher instance. - * - * @var AbstractPasswordHasher - */ - protected $_passwordHasher; - -/** - * Implemented events - * - * @return array of events => callbacks. - */ - public function implementedEvents() { - return array(); - } - -/** - * Constructor - * - * @param ComponentCollection $collection The Component collection used on this request. - * @param array $settings Array of settings to use. - */ - public function __construct(ComponentCollection $collection, $settings) { - $this->_Collection = $collection; - $this->settings = Hash::merge($this->settings, $settings); - } - -/** - * Find a user record using the standard options. - * - * The $username parameter can be a (string)username or an array containing - * conditions for Model::find('first'). If the $password param is not provided - * the password field will be present in returned array. - * - * Input passwords will be hashed even when a user doesn't exist. This - * helps mitigate timing attacks that are attempting to find valid usernames. - * - * @param string|array $username The username/identifier, or an array of find conditions. - * @param string $password The password, only used if $username param is string. - * @return bool|array Either false on failure, or an array of user data. - */ - protected function _findUser($username, $password = null) { - $userModel = $this->settings['userModel']; - list(, $model) = pluginSplit($userModel); - $fields = $this->settings['fields']; - - if (is_array($username)) { - $conditions = $username; - } else { - $conditions = array( - $model . '.' . $fields['username'] => $username - ); - } - - if (!empty($this->settings['scope'])) { - $conditions = array_merge($conditions, $this->settings['scope']); - } - - $userFields = $this->settings['userFields']; - if ($password !== null && $userFields !== null) { - $userFields[] = $model . '.' . $fields['password']; - } - - $result = ClassRegistry::init($userModel)->find('first', array( - 'conditions' => $conditions, - 'recursive' => $this->settings['recursive'], - 'fields' => $userFields, - 'contain' => $this->settings['contain'], - )); - if (empty($result[$model])) { - $this->passwordHasher()->hash($password); - return false; - } - - $user = $result[$model]; - if ($password !== null) { - if (!$this->passwordHasher()->check($password, $user[$fields['password']])) { - return false; - } - unset($user[$fields['password']]); - } - - unset($result[$model]); - return array_merge($user, $result); - } - -/** - * Return password hasher object - * - * @return AbstractPasswordHasher Password hasher instance - * @throws CakeException If password hasher class not found or - * it does not extend AbstractPasswordHasher - */ - public function passwordHasher() { - if ($this->_passwordHasher) { - return $this->_passwordHasher; - } - - $config = array(); - if (is_string($this->settings['passwordHasher'])) { - $class = $this->settings['passwordHasher']; - } else { - $class = $this->settings['passwordHasher']['className']; - $config = $this->settings['passwordHasher']; - unset($config['className']); - } - list($plugin, $class) = pluginSplit($class, true); - $className = $class . 'PasswordHasher'; - App::uses($className, $plugin . 'Controller/Component/Auth'); - if (!class_exists($className)) { - throw new CakeException(__d('cake_dev', 'Password hasher class "%s" was not found.', $class)); - } - if (!is_subclass_of($className, 'AbstractPasswordHasher')) { - throw new CakeException(__d('cake_dev', 'Password hasher must extend AbstractPasswordHasher class.')); - } - $this->_passwordHasher = new $className($config); - return $this->_passwordHasher; - } - -/** - * Hash the plain text password so that it matches the hashed/encrypted password - * in the datasource. - * - * @param string $password The plain text password. - * @return string The hashed form of the password. - * @deprecated 3.0.0 Since 2.4. Use a PasswordHasher class instead. - */ - protected function _password($password) { - return Security::hash($password, null, true); - } - -/** - * Authenticate a user based on the request information. - * - * @param CakeRequest $request Request to get authentication information from. - * @param CakeResponse $response A response object that can have headers added. - * @return mixed Either false on failure, or an array of user data on success. - */ - abstract public function authenticate(CakeRequest $request, CakeResponse $response); - -/** - * Allows you to hook into AuthComponent::logout(), - * and implement specialized logout behavior. - * - * All attached authentication objects will have this method - * called when a user logs out. - * - * @param array $user The user about to be logged out. - * @return void - */ - public function logout($user) { - } - -/** - * Get a user based on information in the request. Primarily used by stateless authentication - * systems like basic and digest auth. - * - * @param CakeRequest $request Request object. - * @return mixed Either false or an array of user information - */ - public function getUser(CakeRequest $request) { - return false; - } - -/** - * Handle unauthenticated access attempt. - * - * @param CakeRequest $request A request object. - * @param CakeResponse $response A response object. - * @return mixed Either true to indicate the unauthenticated request has been - * dealt with and no more action is required by AuthComponent or void (default). - */ - public function unauthenticated(CakeRequest $request, CakeResponse $response) { - } +abstract class BaseAuthenticate implements CakeEventListener +{ + + /** + * Settings for this object. + * + * - `fields` The fields to use to identify a user by. + * - `userModel` The model name of the User, defaults to User. + * - `userFields` Array of fields to retrieve from User model, null to retrieve all. Defaults to null. + * - `scope` Additional conditions to use when looking up and authenticating users, + * i.e. `array('User.is_active' => 1).` + * - `recursive` The value of the recursive key passed to find(). Defaults to 0. + * - `contain` Extra models to contain and store in session. + * - `passwordHasher` Password hasher class. Can be a string specifying class name + * or an array containing `className` key, any other keys will be passed as + * settings to the class. Defaults to 'Simple'. + * + * @var array + */ + public $settings = [ + 'fields' => [ + 'username' => 'username', + 'password' => 'password' + ], + 'userModel' => 'User', + 'userFields' => null, + 'scope' => [], + 'recursive' => 0, + 'contain' => null, + 'passwordHasher' => 'Simple' + ]; + + /** + * A Component collection, used to get more components. + * + * @var ComponentCollection + */ + protected $_Collection; + + /** + * Password hasher instance. + * + * @var AbstractPasswordHasher + */ + protected $_passwordHasher; + + /** + * Constructor + * + * @param ComponentCollection $collection The Component collection used on this request. + * @param array $settings Array of settings to use. + */ + public function __construct(ComponentCollection $collection, $settings) + { + $this->_Collection = $collection; + $this->settings = Hash::merge($this->settings, $settings); + } + + /** + * Implemented events + * + * @return array of events => callbacks. + */ + public function implementedEvents() + { + return []; + } + + /** + * Authenticate a user based on the request information. + * + * @param CakeRequest $request Request to get authentication information from. + * @param CakeResponse $response A response object that can have headers added. + * @return mixed Either false on failure, or an array of user data on success. + */ + abstract public function authenticate(CakeRequest $request, CakeResponse $response); + + /** + * Allows you to hook into AuthComponent::logout(), + * and implement specialized logout behavior. + * + * All attached authentication objects will have this method + * called when a user logs out. + * + * @param array $user The user about to be logged out. + * @return void + */ + public function logout($user) + { + } + + /** + * Get a user based on information in the request. Primarily used by stateless authentication + * systems like basic and digest auth. + * + * @param CakeRequest $request Request object. + * @return mixed Either false or an array of user information + */ + public function getUser(CakeRequest $request) + { + return false; + } + + /** + * Handle unauthenticated access attempt. + * + * @param CakeRequest $request A request object. + * @param CakeResponse $response A response object. + * @return mixed Either true to indicate the unauthenticated request has been + * dealt with and no more action is required by AuthComponent or void (default). + */ + public function unauthenticated(CakeRequest $request, CakeResponse $response) + { + } + + /** + * Find a user record using the standard options. + * + * The $username parameter can be a (string)username or an array containing + * conditions for Model::find('first'). If the $password param is not provided + * the password field will be present in returned array. + * + * Input passwords will be hashed even when a user doesn't exist. This + * helps mitigate timing attacks that are attempting to find valid usernames. + * + * @param string|array $username The username/identifier, or an array of find conditions. + * @param string $password The password, only used if $username param is string. + * @return bool|array Either false on failure, or an array of user data. + */ + protected function _findUser($username, $password = null) + { + $userModel = $this->settings['userModel']; + list(, $model) = pluginSplit($userModel); + $fields = $this->settings['fields']; + + if (is_array($username)) { + $conditions = $username; + } else { + $conditions = [ + $model . '.' . $fields['username'] => $username + ]; + } + + if (!empty($this->settings['scope'])) { + $conditions = array_merge($conditions, $this->settings['scope']); + } + + $userFields = $this->settings['userFields']; + if ($password !== null && $userFields !== null) { + $userFields[] = $model . '.' . $fields['password']; + } + + $result = ClassRegistry::init($userModel)->find('first', [ + 'conditions' => $conditions, + 'recursive' => $this->settings['recursive'], + 'fields' => $userFields, + 'contain' => $this->settings['contain'], + ]); + if (empty($result[$model])) { + $this->passwordHasher()->hash($password); + return false; + } + + $user = $result[$model]; + if ($password !== null) { + if (!$this->passwordHasher()->check($password, $user[$fields['password']])) { + return false; + } + unset($user[$fields['password']]); + } + + unset($result[$model]); + return array_merge($user, $result); + } + + /** + * Return password hasher object + * + * @return AbstractPasswordHasher Password hasher instance + * @throws CakeException If password hasher class not found or + * it does not extend AbstractPasswordHasher + */ + public function passwordHasher() + { + if ($this->_passwordHasher) { + return $this->_passwordHasher; + } + + $config = []; + if (is_string($this->settings['passwordHasher'])) { + $class = $this->settings['passwordHasher']; + } else { + $class = $this->settings['passwordHasher']['className']; + $config = $this->settings['passwordHasher']; + unset($config['className']); + } + list($plugin, $class) = pluginSplit($class, true); + $className = $class . 'PasswordHasher'; + App::uses($className, $plugin . 'Controller/Component/Auth'); + if (!class_exists($className)) { + throw new CakeException(__d('cake_dev', 'Password hasher class "%s" was not found.', $class)); + } + if (!is_subclass_of($className, 'AbstractPasswordHasher')) { + throw new CakeException(__d('cake_dev', 'Password hasher must extend AbstractPasswordHasher class.')); + } + $this->_passwordHasher = new $className($config); + return $this->_passwordHasher; + } + + /** + * Hash the plain text password so that it matches the hashed/encrypted password + * in the datasource. + * + * @param string $password The plain text password. + * @return string The hashed form of the password. + * @deprecated 3.0.0 Since 2.4. Use a PasswordHasher class instead. + */ + protected function _password($password) + { + return Security::hash($password, null, true); + } } diff --git a/lib/Cake/Controller/Component/Auth/BaseAuthorize.php b/lib/Cake/Controller/Component/Auth/BaseAuthorize.php index 1e6a0d68..affcd97d 100755 --- a/lib/Cake/Controller/Component/Auth/BaseAuthorize.php +++ b/lib/Cake/Controller/Component/Auth/BaseAuthorize.php @@ -21,148 +21,151 @@ * @since 2.0 * @see AuthComponent::$authenticate */ -abstract class BaseAuthorize { +abstract class BaseAuthorize +{ -/** - * Controller for the request. - * - * @var Controller - */ - protected $_Controller = null; + /** + * Settings for authorize objects. + * + * - `actionPath` - The path to ACO nodes that contains the nodes for controllers. Used as a prefix + * when calling $this->action(); + * - `actionMap` - Action -> crud mappings. Used by authorization objects that want to map actions to CRUD roles. + * - `userModel` - Model name that ARO records can be found under. Defaults to 'User'. + * + * @var array + */ + public $settings = [ + 'actionPath' => null, + 'actionMap' => [ + 'index' => 'read', + 'add' => 'create', + 'edit' => 'update', + 'view' => 'read', + 'delete' => 'delete', + 'remove' => 'delete' + ], + 'userModel' => 'User' + ]; + /** + * Controller for the request. + * + * @var Controller + */ + protected $_Controller = null; + /** + * Component collection instance for getting more components. + * + * @var ComponentCollection + */ + protected $_Collection; -/** - * Component collection instance for getting more components. - * - * @var ComponentCollection - */ - protected $_Collection; + /** + * Constructor + * + * @param ComponentCollection $collection The controller for this request. + * @param string $settings An array of settings. This class does not use any settings. + */ + public function __construct(ComponentCollection $collection, $settings = []) + { + $this->_Collection = $collection; + $controller = $collection->getController(); + $this->controller($controller); + $this->settings = Hash::merge($this->settings, $settings); + } -/** - * Settings for authorize objects. - * - * - `actionPath` - The path to ACO nodes that contains the nodes for controllers. Used as a prefix - * when calling $this->action(); - * - `actionMap` - Action -> crud mappings. Used by authorization objects that want to map actions to CRUD roles. - * - `userModel` - Model name that ARO records can be found under. Defaults to 'User'. - * - * @var array - */ - public $settings = array( - 'actionPath' => null, - 'actionMap' => array( - 'index' => 'read', - 'add' => 'create', - 'edit' => 'update', - 'view' => 'read', - 'delete' => 'delete', - 'remove' => 'delete' - ), - 'userModel' => 'User' - ); - -/** - * Constructor - * - * @param ComponentCollection $collection The controller for this request. - * @param string $settings An array of settings. This class does not use any settings. - */ - public function __construct(ComponentCollection $collection, $settings = array()) { - $this->_Collection = $collection; - $controller = $collection->getController(); - $this->controller($controller); - $this->settings = Hash::merge($this->settings, $settings); - } - -/** - * Checks user authorization. - * - * @param array $user Active user data - * @param CakeRequest $request Request instance. - * @return bool - */ - abstract public function authorize($user, CakeRequest $request); + /** + * Accessor to the controller object. + * + * @param Controller $controller null to get, a controller to set. + * @return mixed + * @throws CakeException + */ + public function controller(Controller $controller = null) + { + if ($controller) { + if (!$controller instanceof Controller) { + throw new CakeException(__d('cake_dev', '$controller needs to be an instance of Controller')); + } + $this->_Controller = $controller; + return true; + } + return $this->_Controller; + } -/** - * Accessor to the controller object. - * - * @param Controller $controller null to get, a controller to set. - * @return mixed - * @throws CakeException - */ - public function controller(Controller $controller = null) { - if ($controller) { - if (!$controller instanceof Controller) { - throw new CakeException(__d('cake_dev', '$controller needs to be an instance of Controller')); - } - $this->_Controller = $controller; - return true; - } - return $this->_Controller; - } + /** + * Checks user authorization. + * + * @param array $user Active user data + * @param CakeRequest $request Request instance. + * @return bool + */ + abstract public function authorize($user, CakeRequest $request); -/** - * Get the action path for a given request. Primarily used by authorize objects - * that need to get information about the plugin, controller, and action being invoked. - * - * @param CakeRequest $request The request a path is needed for. - * @param string $path Path format. - * @return string the action path for the given request. - */ - public function action(CakeRequest $request, $path = '/:plugin/:controller/:action') { - $plugin = empty($request['plugin']) ? null : Inflector::camelize($request['plugin']) . '/'; - $path = str_replace( - array(':controller', ':action', ':plugin/'), - array(Inflector::camelize($request['controller']), $request['action'], $plugin), - $this->settings['actionPath'] . $path - ); - $path = str_replace('//', '/', $path); - return trim($path, '/'); - } + /** + * Get the action path for a given request. Primarily used by authorize objects + * that need to get information about the plugin, controller, and action being invoked. + * + * @param CakeRequest $request The request a path is needed for. + * @param string $path Path format. + * @return string the action path for the given request. + */ + public function action(CakeRequest $request, $path = '/:plugin/:controller/:action') + { + $plugin = empty($request['plugin']) ? null : Inflector::camelize($request['plugin']) . '/'; + $path = str_replace( + [':controller', ':action', ':plugin/'], + [Inflector::camelize($request['controller']), $request['action'], $plugin], + $this->settings['actionPath'] . $path + ); + $path = str_replace('//', '/', $path); + return trim($path, '/'); + } -/** - * Maps crud actions to actual action names. Used to modify or get the current mapped actions. - * - * Create additional mappings for a standard CRUD operation: - * - * ``` - * $this->Auth->mapActions(array('create' => array('add', 'register')); - * ``` - * - * Or equivalently: - * - * ``` - * $this->Auth->mapActions(array('register' => 'create', 'add' => 'create')); - * ``` - * - * Create mappings for custom CRUD operations: - * - * ``` - * $this->Auth->mapActions(array('range' => 'search')); - * ``` - * - * You can use the custom CRUD operations to create additional generic permissions - * that behave like CRUD operations. Doing this will require additional columns on the - * permissions lookup. For example if one wanted an additional search CRUD operation - * one would create and additional column '_search' in the aros_acos table. One could - * create a custom admin CRUD operation for administration functions similarly if needed. - * - * @param array $map Either an array of mappings, or undefined to get current values. - * @return mixed Either the current mappings or null when setting. - * @see AuthComponent::mapActions() - */ - public function mapActions($map = array()) { - if (empty($map)) { - return $this->settings['actionMap']; - } - foreach ($map as $action => $type) { - if (is_array($type)) { - foreach ($type as $typedAction) { - $this->settings['actionMap'][$typedAction] = $action; - } - } else { - $this->settings['actionMap'][$action] = $type; - } - } - } + /** + * Maps crud actions to actual action names. Used to modify or get the current mapped actions. + * + * Create additional mappings for a standard CRUD operation: + * + * ``` + * $this->Auth->mapActions(array('create' => array('add', 'register')); + * ``` + * + * Or equivalently: + * + * ``` + * $this->Auth->mapActions(array('register' => 'create', 'add' => 'create')); + * ``` + * + * Create mappings for custom CRUD operations: + * + * ``` + * $this->Auth->mapActions(array('range' => 'search')); + * ``` + * + * You can use the custom CRUD operations to create additional generic permissions + * that behave like CRUD operations. Doing this will require additional columns on the + * permissions lookup. For example if one wanted an additional search CRUD operation + * one would create and additional column '_search' in the aros_acos table. One could + * create a custom admin CRUD operation for administration functions similarly if needed. + * + * @param array $map Either an array of mappings, or undefined to get current values. + * @return mixed Either the current mappings or null when setting. + * @see AuthComponent::mapActions() + */ + public function mapActions($map = []) + { + if (empty($map)) { + return $this->settings['actionMap']; + } + foreach ($map as $action => $type) { + if (is_array($type)) { + foreach ($type as $typedAction) { + $this->settings['actionMap'][$typedAction] = $action; + } + } else { + $this->settings['actionMap'][$action] = $type; + } + } + } } diff --git a/lib/Cake/Controller/Component/Auth/BasicAuthenticate.php b/lib/Cake/Controller/Component/Auth/BasicAuthenticate.php index b19f23c9..98d180fb 100755 --- a/lib/Cake/Controller/Component/Auth/BasicAuthenticate.php +++ b/lib/Cake/Controller/Component/Auth/BasicAuthenticate.php @@ -25,11 +25,11 @@ * * In your controller's components array, add auth + the required settings. * ``` - * public $components = array( - * 'Auth' => array( - * 'authenticate' => array('Basic') - * ) - * ); + * public $components = array( + * 'Auth' => array( + * 'authenticate' => array('Basic') + * ) + * ); * ``` * * You should also set `AuthComponent::$sessionKey = false;` in your AppController's @@ -48,76 +48,82 @@ * @package Cake.Controller.Component.Auth * @since 2.0 */ -class BasicAuthenticate extends BaseAuthenticate { +class BasicAuthenticate extends BaseAuthenticate +{ -/** - * Constructor, completes configuration for basic authentication. - * - * @param ComponentCollection $collection The Component collection used on this request. - * @param array $settings An array of settings. - */ - public function __construct(ComponentCollection $collection, $settings) { - parent::__construct($collection, $settings); - if (empty($this->settings['realm'])) { - $this->settings['realm'] = env('SERVER_NAME'); - } - } + /** + * Constructor, completes configuration for basic authentication. + * + * @param ComponentCollection $collection The Component collection used on this request. + * @param array $settings An array of settings. + */ + public function __construct(ComponentCollection $collection, $settings) + { + parent::__construct($collection, $settings); + if (empty($this->settings['realm'])) { + $this->settings['realm'] = env('SERVER_NAME'); + } + } -/** - * Authenticate a user using HTTP auth. Will use the configured User model and attempt a - * login using HTTP auth. - * - * @param CakeRequest $request The request to authenticate with. - * @param CakeResponse $response The response to add headers to. - * @return mixed Either false on failure, or an array of user data on success. - */ - public function authenticate(CakeRequest $request, CakeResponse $response) { - return $this->getUser($request); - } + /** + * Authenticate a user using HTTP auth. Will use the configured User model and attempt a + * login using HTTP auth. + * + * @param CakeRequest $request The request to authenticate with. + * @param CakeResponse $response The response to add headers to. + * @return mixed Either false on failure, or an array of user data on success. + */ + public function authenticate(CakeRequest $request, CakeResponse $response) + { + return $this->getUser($request); + } -/** - * Get a user based on information in the request. Used by cookie-less auth for stateless clients. - * - * @param CakeRequest $request Request object. - * @return mixed Either false or an array of user information - */ - public function getUser(CakeRequest $request) { - $username = env('PHP_AUTH_USER'); - $pass = env('PHP_AUTH_PW'); - if (!strlen($username)) { - $httpAuthorization = $request->header('Authorization'); - if (strlen($httpAuthorization) > 0 && strpos($httpAuthorization, 'Basic') !== false) { - list($username, $pass) = explode(':', base64_decode(substr($httpAuthorization, 6))); - } - } + /** + * Get a user based on information in the request. Used by cookie-less auth for stateless clients. + * + * @param CakeRequest $request Request object. + * @return mixed Either false or an array of user information + */ + public function getUser(CakeRequest $request) + { + $username = env('PHP_AUTH_USER'); + $pass = env('PHP_AUTH_PW'); + if (!strlen($username)) { + $httpAuthorization = $request->header('Authorization'); + if (strlen($httpAuthorization) > 0 && strpos($httpAuthorization, 'Basic') !== false) { + list($username, $pass) = explode(':', base64_decode(substr($httpAuthorization, 6))); + } + } - if (!is_string($username) || $username === '' || !is_string($pass) || $pass === '') { - return false; - } - return $this->_findUser($username, $pass); - } + if (!is_string($username) || $username === '' || !is_string($pass) || $pass === '') { + return false; + } + return $this->_findUser($username, $pass); + } -/** - * Handles an unauthenticated access attempt by sending appropriate login headers - * - * @param CakeRequest $request A request object. - * @param CakeResponse $response A response object. - * @return void - * @throws UnauthorizedException - */ - public function unauthenticated(CakeRequest $request, CakeResponse $response) { - $Exception = new UnauthorizedException(); - $Exception->responseHeader(array($this->loginHeaders())); - throw $Exception; - } + /** + * Handles an unauthenticated access attempt by sending appropriate login headers + * + * @param CakeRequest $request A request object. + * @param CakeResponse $response A response object. + * @return void + * @throws UnauthorizedException + */ + public function unauthenticated(CakeRequest $request, CakeResponse $response) + { + $Exception = new UnauthorizedException(); + $Exception->responseHeader([$this->loginHeaders()]); + throw $Exception; + } -/** - * Generate the login headers - * - * @return string Headers for logging in. - */ - public function loginHeaders() { - return sprintf('WWW-Authenticate: Basic realm="%s"', $this->settings['realm']); - } + /** + * Generate the login headers + * + * @return string Headers for logging in. + */ + public function loginHeaders() + { + return sprintf('WWW-Authenticate: Basic realm="%s"', $this->settings['realm']); + } } diff --git a/lib/Cake/Controller/Component/Auth/BlowfishAuthenticate.php b/lib/Cake/Controller/Component/Auth/BlowfishAuthenticate.php index 16fbcaa1..60db1f2b 100755 --- a/lib/Cake/Controller/Component/Auth/BlowfishAuthenticate.php +++ b/lib/Cake/Controller/Component/Auth/BlowfishAuthenticate.php @@ -19,11 +19,11 @@ * hashing. Can be used by configuring AuthComponent to use it via the AuthComponent::$authenticate setting. * * ``` - * $this->Auth->authenticate = array( - * 'Blowfish' => array( - * 'scope' => array('User.active' => 1) - * ) - * ) + * $this->Auth->authenticate = array( + * 'Blowfish' => array( + * 'scope' => array('User.active' => 1) + * ) + * ) * ``` * * When configuring BlowfishAuthenticate you can pass in settings to which fields, model and additional conditions @@ -32,22 +32,24 @@ * For initial password hashing/creation see Security::hash(). Other than how the password is initially hashed, * BlowfishAuthenticate works exactly the same way as FormAuthenticate. * - * @package Cake.Controller.Component.Auth + * @package Cake.Controller.Component.Auth * @since CakePHP(tm) v 2.3 - * @see AuthComponent::$authenticate + * @see AuthComponent::$authenticate * @deprecated 3.0.0 Since 2.4. Just use FormAuthenticate with 'passwordHasher' setting set to 'Blowfish' */ -class BlowfishAuthenticate extends FormAuthenticate { +class BlowfishAuthenticate extends FormAuthenticate +{ -/** - * Constructor. Sets default passwordHasher to Blowfish - * - * @param ComponentCollection $collection The Component collection used on this request. - * @param array $settings Array of settings to use. - */ - public function __construct(ComponentCollection $collection, $settings) { - $this->settings['passwordHasher'] = 'Blowfish'; - parent::__construct($collection, $settings); - } + /** + * Constructor. Sets default passwordHasher to Blowfish + * + * @param ComponentCollection $collection The Component collection used on this request. + * @param array $settings Array of settings to use. + */ + public function __construct(ComponentCollection $collection, $settings) + { + $this->settings['passwordHasher'] = 'Blowfish'; + parent::__construct($collection, $settings); + } } diff --git a/lib/Cake/Controller/Component/Auth/BlowfishPasswordHasher.php b/lib/Cake/Controller/Component/Auth/BlowfishPasswordHasher.php index ced01636..ef2f281d 100755 --- a/lib/Cake/Controller/Component/Auth/BlowfishPasswordHasher.php +++ b/lib/Cake/Controller/Component/Auth/BlowfishPasswordHasher.php @@ -21,28 +21,31 @@ * * @package Cake.Controller.Component.Auth */ -class BlowfishPasswordHasher extends AbstractPasswordHasher { +class BlowfishPasswordHasher extends AbstractPasswordHasher +{ -/** - * Generates password hash. - * - * @param string $password Plain text password to hash. - * @return string Password hash - * @link https://book.cakephp.org/2.0/en/core-libraries/components/authentication.html#using-bcrypt-for-passwords - */ - public function hash($password) { - return Security::hash($password, 'blowfish', false); - } + /** + * Generates password hash. + * + * @param string $password Plain text password to hash. + * @return string Password hash + * @link https://book.cakephp.org/2.0/en/core-libraries/components/authentication.html#using-bcrypt-for-passwords + */ + public function hash($password) + { + return Security::hash($password, 'blowfish', false); + } -/** - * Check hash. Generate hash for user provided password and check against existing hash. - * - * @param string $password Plain text password to hash. - * @param string $hashedPassword Existing hashed password. - * @return bool True if hashes match else false. - */ - public function check($password, $hashedPassword) { - return $hashedPassword === Security::hash($password, 'blowfish', $hashedPassword); - } + /** + * Check hash. Generate hash for user provided password and check against existing hash. + * + * @param string $password Plain text password to hash. + * @param string $hashedPassword Existing hashed password. + * @return bool True if hashes match else false. + */ + public function check($password, $hashedPassword) + { + return $hashedPassword === Security::hash($password, 'blowfish', $hashedPassword); + } } diff --git a/lib/Cake/Controller/Component/Auth/ControllerAuthorize.php b/lib/Cake/Controller/Component/Auth/ControllerAuthorize.php index c55ef3d3..056c1edf 100755 --- a/lib/Cake/Controller/Component/Auth/ControllerAuthorize.php +++ b/lib/Cake/Controller/Component/Auth/ControllerAuthorize.php @@ -19,12 +19,12 @@ * Your controller's isAuthorized() method should return a boolean to indicate whether or not the user is authorized. * * ``` - * public function isAuthorized($user) { - * if (!empty($this->request->params['admin'])) { - * return $user['role'] === 'admin'; - * } - * return !empty($user); - * } + * public function isAuthorized($user) { + * if (!empty($this->request->params['admin'])) { + * return $user['role'] === 'admin'; + * } + * return !empty($user); + * } * ``` * * the above is simple implementation that would only authorize users of the 'admin' role to access @@ -34,33 +34,36 @@ * @since 2.0 * @see AuthComponent::$authenticate */ -class ControllerAuthorize extends BaseAuthorize { +class ControllerAuthorize extends BaseAuthorize +{ -/** - * Get/set the controller this authorize object will be working with. Also checks that isAuthorized is implemented. - * - * @param Controller $controller null to get, a controller to set. - * @return mixed - * @throws CakeException - */ - public function controller(Controller $controller = null) { - if ($controller) { - if (!method_exists($controller, 'isAuthorized')) { - throw new CakeException(__d('cake_dev', '$controller does not implement an %s method.', 'isAuthorized()')); - } - } - return parent::controller($controller); - } + /** + * Get/set the controller this authorize object will be working with. Also checks that isAuthorized is implemented. + * + * @param Controller $controller null to get, a controller to set. + * @return mixed + * @throws CakeException + */ + public function controller(Controller $controller = null) + { + if ($controller) { + if (!method_exists($controller, 'isAuthorized')) { + throw new CakeException(__d('cake_dev', '$controller does not implement an %s method.', 'isAuthorized()')); + } + } + return parent::controller($controller); + } -/** - * Checks user authorization using a controller callback. - * - * @param array $user Active user data - * @param CakeRequest $request Request instance. - * @return bool - */ - public function authorize($user, CakeRequest $request) { - return (bool)$this->_Controller->isAuthorized($user); - } + /** + * Checks user authorization using a controller callback. + * + * @param array $user Active user data + * @param CakeRequest $request Request instance. + * @return bool + */ + public function authorize($user, CakeRequest $request) + { + return (bool)$this->_Controller->isAuthorized($user); + } } diff --git a/lib/Cake/Controller/Component/Auth/CrudAuthorize.php b/lib/Cake/Controller/Component/Auth/CrudAuthorize.php index 6eb06f33..2193ae10 100755 --- a/lib/Cake/Controller/Component/Auth/CrudAuthorize.php +++ b/lib/Cake/Controller/Component/Auth/CrudAuthorize.php @@ -30,72 +30,76 @@ * @see AuthComponent::$authenticate * @see AclComponent::check() */ -class CrudAuthorize extends BaseAuthorize { +class CrudAuthorize extends BaseAuthorize +{ -/** - * Sets up additional actionMap values that match the configured `Routing.prefixes`. - * - * @param ComponentCollection $collection The component collection from the controller. - * @param string $settings An array of settings. This class does not use any settings. - */ - public function __construct(ComponentCollection $collection, $settings = array()) { - parent::__construct($collection, $settings); - $this->_setPrefixMappings(); - } + /** + * Sets up additional actionMap values that match the configured `Routing.prefixes`. + * + * @param ComponentCollection $collection The component collection from the controller. + * @param string $settings An array of settings. This class does not use any settings. + */ + public function __construct(ComponentCollection $collection, $settings = []) + { + parent::__construct($collection, $settings); + $this->_setPrefixMappings(); + } -/** - * sets the crud mappings for prefix routes. - * - * @return void - */ - protected function _setPrefixMappings() { - $crud = array('create', 'read', 'update', 'delete'); - $map = array_combine($crud, $crud); + /** + * sets the crud mappings for prefix routes. + * + * @return void + */ + protected function _setPrefixMappings() + { + $crud = ['create', 'read', 'update', 'delete']; + $map = array_combine($crud, $crud); - $prefixes = Router::prefixes(); - if (!empty($prefixes)) { - foreach ($prefixes as $prefix) { - $map = array_merge($map, array( - $prefix . '_index' => 'read', - $prefix . '_add' => 'create', - $prefix . '_edit' => 'update', - $prefix . '_view' => 'read', - $prefix . '_remove' => 'delete', - $prefix . '_create' => 'create', - $prefix . '_read' => 'read', - $prefix . '_update' => 'update', - $prefix . '_delete' => 'delete' - )); - } - } - $this->mapActions($map); - } + $prefixes = Router::prefixes(); + if (!empty($prefixes)) { + foreach ($prefixes as $prefix) { + $map = array_merge($map, [ + $prefix . '_index' => 'read', + $prefix . '_add' => 'create', + $prefix . '_edit' => 'update', + $prefix . '_view' => 'read', + $prefix . '_remove' => 'delete', + $prefix . '_create' => 'create', + $prefix . '_read' => 'read', + $prefix . '_update' => 'update', + $prefix . '_delete' => 'delete' + ]); + } + } + $this->mapActions($map); + } -/** - * Authorize a user using the mapped actions and the AclComponent. - * - * @param array $user The user to authorize - * @param CakeRequest $request The request needing authorization. - * @return bool - */ - public function authorize($user, CakeRequest $request) { - if (!isset($this->settings['actionMap'][$request->params['action']])) { - trigger_error(__d('cake_dev', - 'CrudAuthorize::authorize() - Attempted access of un-mapped action "%1$s" in controller "%2$s"', - $request->action, - $request->controller - ), - E_USER_WARNING - ); - return false; - } - $user = array($this->settings['userModel'] => $user); - $Acl = $this->_Collection->load('Acl'); - return $Acl->check( - $user, - $this->action($request, ':controller'), - $this->settings['actionMap'][$request->params['action']] - ); - } + /** + * Authorize a user using the mapped actions and the AclComponent. + * + * @param array $user The user to authorize + * @param CakeRequest $request The request needing authorization. + * @return bool + */ + public function authorize($user, CakeRequest $request) + { + if (!isset($this->settings['actionMap'][$request->params['action']])) { + trigger_error(__d('cake_dev', + 'CrudAuthorize::authorize() - Attempted access of un-mapped action "%1$s" in controller "%2$s"', + $request->action, + $request->controller + ), + E_USER_WARNING + ); + return false; + } + $user = [$this->settings['userModel'] => $user]; + $Acl = $this->_Collection->load('Acl'); + return $Acl->check( + $user, + $this->action($request, ':controller'), + $this->settings['actionMap'][$request->params['action']] + ); + } } diff --git a/lib/Cake/Controller/Component/Auth/DigestAuthenticate.php b/lib/Cake/Controller/Component/Auth/DigestAuthenticate.php index 04637994..577023d1 100755 --- a/lib/Cake/Controller/Component/Auth/DigestAuthenticate.php +++ b/lib/Cake/Controller/Component/Auth/DigestAuthenticate.php @@ -29,11 +29,11 @@ * * In your controller's components array, add auth + the required settings. * ``` - * public $components = array( - * 'Auth' => array( - * 'authenticate' => array('Digest') - * ) - * ); + * public $components = array( + * 'Auth' => array( + * 'authenticate' => array('Digest') + * ) + * ); * ``` * * In your login function just call `$this->Auth->login()` without any checks for POST data. This @@ -53,174 +53,182 @@ * @package Cake.Controller.Component.Auth * @since 2.0 */ -class DigestAuthenticate extends BasicAuthenticate { +class DigestAuthenticate extends BasicAuthenticate +{ -/** - * Settings for this object. - * - * - `fields` The fields to use to identify a user by. - * - `userModel` The model name of the User, defaults to User. - * - `userFields` Array of fields to retrieve from User model, null to retrieve all. Defaults to null. - * - `scope` Additional conditions to use when looking up and authenticating users, - * i.e. `array('User.is_active' => 1).` - * - `recursive` The value of the recursive key passed to find(). Defaults to 0. - * - `contain` Extra models to contain and store in session. - * - `realm` The realm authentication is for, Defaults to the servername. - * - `nonce` A nonce used for authentication. Defaults to `uniqid()`. - * - `qop` Defaults to auth, no other values are supported at this time. - * - `opaque` A string that must be returned unchanged by clients. - * Defaults to `md5($settings['realm'])` - * - * @var array - */ - public $settings = array( - 'fields' => array( - 'username' => 'username', - 'password' => 'password' - ), - 'userModel' => 'User', - 'userFields' => null, - 'scope' => array(), - 'recursive' => 0, - 'contain' => null, - 'realm' => '', - 'qop' => 'auth', - 'nonce' => '', - 'opaque' => '', - 'passwordHasher' => 'Simple', - ); + /** + * Settings for this object. + * + * - `fields` The fields to use to identify a user by. + * - `userModel` The model name of the User, defaults to User. + * - `userFields` Array of fields to retrieve from User model, null to retrieve all. Defaults to null. + * - `scope` Additional conditions to use when looking up and authenticating users, + * i.e. `array('User.is_active' => 1).` + * - `recursive` The value of the recursive key passed to find(). Defaults to 0. + * - `contain` Extra models to contain and store in session. + * - `realm` The realm authentication is for, Defaults to the servername. + * - `nonce` A nonce used for authentication. Defaults to `uniqid()`. + * - `qop` Defaults to auth, no other values are supported at this time. + * - `opaque` A string that must be returned unchanged by clients. + * Defaults to `md5($settings['realm'])` + * + * @var array + */ + public $settings = [ + 'fields' => [ + 'username' => 'username', + 'password' => 'password' + ], + 'userModel' => 'User', + 'userFields' => null, + 'scope' => [], + 'recursive' => 0, + 'contain' => null, + 'realm' => '', + 'qop' => 'auth', + 'nonce' => '', + 'opaque' => '', + 'passwordHasher' => 'Simple', + ]; -/** - * Constructor, completes configuration for digest authentication. - * - * @param ComponentCollection $collection The Component collection used on this request. - * @param array $settings An array of settings. - */ - public function __construct(ComponentCollection $collection, $settings) { - parent::__construct($collection, $settings); - if (empty($this->settings['nonce'])) { - $this->settings['nonce'] = uniqid(''); - } - if (empty($this->settings['opaque'])) { - $this->settings['opaque'] = md5($this->settings['realm']); - } - } + /** + * Constructor, completes configuration for digest authentication. + * + * @param ComponentCollection $collection The Component collection used on this request. + * @param array $settings An array of settings. + */ + public function __construct(ComponentCollection $collection, $settings) + { + parent::__construct($collection, $settings); + if (empty($this->settings['nonce'])) { + $this->settings['nonce'] = uniqid(''); + } + if (empty($this->settings['opaque'])) { + $this->settings['opaque'] = md5($this->settings['realm']); + } + } -/** - * Get a user based on information in the request. Used by cookie-less auth for stateless clients. - * - * @param CakeRequest $request Request object. - * @return mixed Either false or an array of user information - */ - public function getUser(CakeRequest $request) { - $digest = $this->_getDigest(); - if (empty($digest)) { - return false; - } + /** + * Creates an auth digest password hash to store + * + * @param string $username The username to use in the digest hash. + * @param string $password The unhashed password to make a digest hash for. + * @param string $realm The realm the password is for. + * @return string the hashed password that can later be used with Digest authentication. + */ + public static function password($username, $password, $realm) + { + return md5($username . ':' . $realm . ':' . $password); + } - list(, $model) = pluginSplit($this->settings['userModel']); - $user = $this->_findUser(array( - $model . '.' . $this->settings['fields']['username'] => $digest['username'] - )); - if (empty($user)) { - return false; - } - $password = $user[$this->settings['fields']['password']]; - unset($user[$this->settings['fields']['password']]); - if ($digest['response'] === $this->generateResponseHash($digest, $password)) { - return $user; - } - return false; - } + /** + * Get a user based on information in the request. Used by cookie-less auth for stateless clients. + * + * @param CakeRequest $request Request object. + * @return mixed Either false or an array of user information + */ + public function getUser(CakeRequest $request) + { + $digest = $this->_getDigest(); + if (empty($digest)) { + return false; + } -/** - * Gets the digest headers from the request/environment. - * - * @return array|bool|null Array of digest information. - */ - protected function _getDigest() { - $digest = env('PHP_AUTH_DIGEST'); - if (empty($digest) && function_exists('apache_request_headers')) { - $headers = apache_request_headers(); - if (!empty($headers['Authorization']) && substr($headers['Authorization'], 0, 7) === 'Digest ') { - $digest = substr($headers['Authorization'], 7); - } - } - if (empty($digest)) { - return false; - } - return $this->parseAuthData($digest); - } + list(, $model) = pluginSplit($this->settings['userModel']); + $user = $this->_findUser([ + $model . '.' . $this->settings['fields']['username'] => $digest['username'] + ]); + if (empty($user)) { + return false; + } + $password = $user[$this->settings['fields']['password']]; + unset($user[$this->settings['fields']['password']]); + if ($digest['response'] === $this->generateResponseHash($digest, $password)) { + return $user; + } + return false; + } -/** - * Parse the digest authentication headers and split them up. - * - * @param string $digest The raw digest authentication headers. - * @return array|null An array of digest authentication headers - */ - public function parseAuthData($digest) { - if (substr($digest, 0, 7) === 'Digest ') { - $digest = substr($digest, 7); - } - $keys = $match = array(); - $req = array('nonce' => 1, 'nc' => 1, 'cnonce' => 1, 'qop' => 1, 'username' => 1, 'uri' => 1, 'response' => 1); - preg_match_all('/(\w+)=([\'"]?)([a-zA-Z0-9\:\#\%\?\&@=\.\/_-]+)\2/', $digest, $match, PREG_SET_ORDER); + /** + * Gets the digest headers from the request/environment. + * + * @return array|bool|null Array of digest information. + */ + protected function _getDigest() + { + $digest = env('PHP_AUTH_DIGEST'); + if (empty($digest) && function_exists('apache_request_headers')) { + $headers = apache_request_headers(); + if (!empty($headers['Authorization']) && substr($headers['Authorization'], 0, 7) === 'Digest ') { + $digest = substr($headers['Authorization'], 7); + } + } + if (empty($digest)) { + return false; + } + return $this->parseAuthData($digest); + } - foreach ($match as $i) { - $keys[$i[1]] = $i[3]; - unset($req[$i[1]]); - } + /** + * Parse the digest authentication headers and split them up. + * + * @param string $digest The raw digest authentication headers. + * @return array|null An array of digest authentication headers + */ + public function parseAuthData($digest) + { + if (substr($digest, 0, 7) === 'Digest ') { + $digest = substr($digest, 7); + } + $keys = $match = []; + $req = ['nonce' => 1, 'nc' => 1, 'cnonce' => 1, 'qop' => 1, 'username' => 1, 'uri' => 1, 'response' => 1]; + preg_match_all('/(\w+)=([\'"]?)([a-zA-Z0-9\:\#\%\?\&@=\.\/_-]+)\2/', $digest, $match, PREG_SET_ORDER); - if (empty($req)) { - return $keys; - } - return null; - } + foreach ($match as $i) { + $keys[$i[1]] = $i[3]; + unset($req[$i[1]]); + } -/** - * Generate the response hash for a given digest array. - * - * @param array $digest Digest information containing data from DigestAuthenticate::parseAuthData(). - * @param string $password The digest hash password generated with DigestAuthenticate::password() - * @return string Response hash - */ - public function generateResponseHash($digest, $password) { - return md5( - $password . - ':' . $digest['nonce'] . ':' . $digest['nc'] . ':' . $digest['cnonce'] . ':' . $digest['qop'] . ':' . - md5(env('REQUEST_METHOD') . ':' . $digest['uri']) - ); - } + if (empty($req)) { + return $keys; + } + return null; + } -/** - * Creates an auth digest password hash to store - * - * @param string $username The username to use in the digest hash. - * @param string $password The unhashed password to make a digest hash for. - * @param string $realm The realm the password is for. - * @return string the hashed password that can later be used with Digest authentication. - */ - public static function password($username, $password, $realm) { - return md5($username . ':' . $realm . ':' . $password); - } + /** + * Generate the response hash for a given digest array. + * + * @param array $digest Digest information containing data from DigestAuthenticate::parseAuthData(). + * @param string $password The digest hash password generated with DigestAuthenticate::password() + * @return string Response hash + */ + public function generateResponseHash($digest, $password) + { + return md5( + $password . + ':' . $digest['nonce'] . ':' . $digest['nc'] . ':' . $digest['cnonce'] . ':' . $digest['qop'] . ':' . + md5(env('REQUEST_METHOD') . ':' . $digest['uri']) + ); + } -/** - * Generate the login headers - * - * @return string Headers for logging in. - */ - public function loginHeaders() { - $options = array( - 'realm' => $this->settings['realm'], - 'qop' => $this->settings['qop'], - 'nonce' => $this->settings['nonce'], - 'opaque' => $this->settings['opaque'] - ); - $opts = array(); - foreach ($options as $k => $v) { - $opts[] = sprintf('%s="%s"', $k, $v); - } - return 'WWW-Authenticate: Digest ' . implode(',', $opts); - } + /** + * Generate the login headers + * + * @return string Headers for logging in. + */ + public function loginHeaders() + { + $options = [ + 'realm' => $this->settings['realm'], + 'qop' => $this->settings['qop'], + 'nonce' => $this->settings['nonce'], + 'opaque' => $this->settings['opaque'] + ]; + $opts = []; + foreach ($options as $k => $v) { + $opts[] = sprintf('%s="%s"', $k, $v); + } + return 'WWW-Authenticate: Digest ' . implode(',', $opts); + } } diff --git a/lib/Cake/Controller/Component/Auth/FormAuthenticate.php b/lib/Cake/Controller/Component/Auth/FormAuthenticate.php index 849673f7..df8b372d 100755 --- a/lib/Cake/Controller/Component/Auth/FormAuthenticate.php +++ b/lib/Cake/Controller/Component/Auth/FormAuthenticate.php @@ -19,11 +19,11 @@ * data. Can be used by configuring AuthComponent to use it via the AuthComponent::$authenticate setting. * * ``` - * $this->Auth->authenticate = array( - * 'Form' => array( - * 'scope' => array('User.active' => 1) - * ) - * ) + * $this->Auth->authenticate = array( + * 'Form' => array( + * 'scope' => array('User.active' => 1) + * ) + * ) * ``` * * When configuring FormAuthenticate you can pass in settings to which fields, model and additional conditions @@ -33,50 +33,53 @@ * @since 2.0 * @see AuthComponent::$authenticate */ -class FormAuthenticate extends BaseAuthenticate { +class FormAuthenticate extends BaseAuthenticate +{ -/** - * Checks the fields to ensure they are supplied. - * - * @param CakeRequest $request The request that contains login information. - * @param string $model The model used for login verification. - * @param array $fields The fields to be checked. - * @return bool False if the fields have not been supplied. True if they exist. - */ - protected function _checkFields(CakeRequest $request, $model, $fields) { - if (empty($request->data[$model])) { - return false; - } - foreach (array($fields['username'], $fields['password']) as $field) { - $value = $request->data($model . '.' . $field); - if (empty($value) && $value !== '0' || !is_string($value)) { - return false; - } - } - return true; - } + /** + * Authenticates the identity contained in a request. Will use the `settings.userModel`, and `settings.fields` + * to find POST data that is used to find a matching record in the `settings.userModel`. Will return false if + * there is no post data, either username or password is missing, or if the scope conditions have not been met. + * + * @param CakeRequest $request The request that contains login information. + * @param CakeResponse $response Unused response object. + * @return mixed False on login failure. An array of User data on success. + */ + public function authenticate(CakeRequest $request, CakeResponse $response) + { + $userModel = $this->settings['userModel']; + list(, $model) = pluginSplit($userModel); -/** - * Authenticates the identity contained in a request. Will use the `settings.userModel`, and `settings.fields` - * to find POST data that is used to find a matching record in the `settings.userModel`. Will return false if - * there is no post data, either username or password is missing, or if the scope conditions have not been met. - * - * @param CakeRequest $request The request that contains login information. - * @param CakeResponse $response Unused response object. - * @return mixed False on login failure. An array of User data on success. - */ - public function authenticate(CakeRequest $request, CakeResponse $response) { - $userModel = $this->settings['userModel']; - list(, $model) = pluginSplit($userModel); + $fields = $this->settings['fields']; + if (!$this->_checkFields($request, $model, $fields)) { + return false; + } + return $this->_findUser( + $request->data[$model][$fields['username']], + $request->data[$model][$fields['password']] + ); + } - $fields = $this->settings['fields']; - if (!$this->_checkFields($request, $model, $fields)) { - return false; - } - return $this->_findUser( - $request->data[$model][$fields['username']], - $request->data[$model][$fields['password']] - ); - } + /** + * Checks the fields to ensure they are supplied. + * + * @param CakeRequest $request The request that contains login information. + * @param string $model The model used for login verification. + * @param array $fields The fields to be checked. + * @return bool False if the fields have not been supplied. True if they exist. + */ + protected function _checkFields(CakeRequest $request, $model, $fields) + { + if (empty($request->data[$model])) { + return false; + } + foreach ([$fields['username'], $fields['password']] as $field) { + $value = $request->data($model . '.' . $field); + if (empty($value) && $value !== '0' || !is_string($value)) { + return false; + } + } + return true; + } } diff --git a/lib/Cake/Controller/Component/Auth/SimplePasswordHasher.php b/lib/Cake/Controller/Component/Auth/SimplePasswordHasher.php index 53e381a5..6bcd69f1 100755 --- a/lib/Cake/Controller/Component/Auth/SimplePasswordHasher.php +++ b/lib/Cake/Controller/Component/Auth/SimplePasswordHasher.php @@ -21,35 +21,38 @@ * * @package Cake.Controller.Component.Auth */ -class SimplePasswordHasher extends AbstractPasswordHasher { +class SimplePasswordHasher extends AbstractPasswordHasher +{ -/** - * Config for this object. - * - * @var array - */ - protected $_config = array('hashType' => null); + /** + * Config for this object. + * + * @var array + */ + protected $_config = ['hashType' => null]; -/** - * Generates password hash. - * - * @param string $password Plain text password to hash. - * @return string Password hash - * @link https://book.cakephp.org/2.0/en/core-libraries/components/authentication.html#hashing-passwords - */ - public function hash($password) { - return Security::hash($password, $this->_config['hashType'], true); - } + /** + * Check hash. Generate hash for user provided password and check against existing hash. + * + * @param string $password Plain text password to hash. + * @param string $hashedPassword Existing hashed password. + * @return bool True if hashes match else false. + */ + public function check($password, $hashedPassword) + { + return $hashedPassword === $this->hash($password); + } -/** - * Check hash. Generate hash for user provided password and check against existing hash. - * - * @param string $password Plain text password to hash. - * @param string $hashedPassword Existing hashed password. - * @return bool True if hashes match else false. - */ - public function check($password, $hashedPassword) { - return $hashedPassword === $this->hash($password); - } + /** + * Generates password hash. + * + * @param string $password Plain text password to hash. + * @return string Password hash + * @link https://book.cakephp.org/2.0/en/core-libraries/components/authentication.html#hashing-passwords + */ + public function hash($password) + { + return Security::hash($password, $this->_config['hashType'], true); + } } diff --git a/lib/Cake/Controller/Component/AuthComponent.php b/lib/Cake/Controller/Component/AuthComponent.php index a652e950..114f6142 100755 --- a/lib/Cake/Controller/Component/AuthComponent.php +++ b/lib/Cake/Controller/Component/AuthComponent.php @@ -36,821 +36,828 @@ * @package Cake.Controller.Component * @link https://book.cakephp.org/2.0/en/core-libraries/components/authentication.html */ -class AuthComponent extends Component { - -/** - * Constant for 'all' - * - * @var string - */ - const ALL = 'all'; - -/** - * Other components utilized by AuthComponent - * - * @var array - */ - public $components = array('Session', 'Flash', 'RequestHandler'); - -/** - * An array of authentication objects to use for authenticating users. You can configure - * multiple adapters and they will be checked sequentially when users are identified. - * - * ``` - * $this->Auth->authenticate = array( - * 'Form' => array( - * 'userModel' => 'Users.User' - * ) - * ); - * ``` - * - * Using the class name without 'Authenticate' as the key, you can pass in an array of settings for each - * authentication object. Additionally you can define settings that should be set to all authentications objects - * using the 'all' key: - * - * ``` - * $this->Auth->authenticate = array( - * 'all' => array( - * 'userModel' => 'Users.User', - * 'scope' => array('User.active' => 1) - * ), - * 'Form', - * 'Basic' - * ); - * ``` - * - * You can also use AuthComponent::ALL instead of the string 'all'. - * - * @var array - * @link https://book.cakephp.org/2.0/en/core-libraries/components/authentication.html - */ - public $authenticate = array('Form'); - -/** - * Objects that will be used for authentication checks. - * - * @var BaseAuthenticate[] - */ - protected $_authenticateObjects = array(); - -/** - * An array of authorization objects to use for authorizing users. You can configure - * multiple adapters and they will be checked sequentially when authorization checks are done. - * - * ``` - * $this->Auth->authorize = array( - * 'Crud' => array( - * 'actionPath' => 'controllers/' - * ) - * ); - * ``` - * - * Using the class name without 'Authorize' as the key, you can pass in an array of settings for each - * authorization object. Additionally you can define settings that should be set to all authorization objects - * using the 'all' key: - * - * ``` - * $this->Auth->authorize = array( - * 'all' => array( - * 'actionPath' => 'controllers/' - * ), - * 'Crud', - * 'CustomAuth' - * ); - * ``` - * - * You can also use AuthComponent::ALL instead of the string 'all' - * - * @var mixed - * @link https://book.cakephp.org/2.0/en/core-libraries/components/authentication.html#authorization - */ - public $authorize = false; - -/** - * Objects that will be used for authorization checks. - * - * @var BaseAuthorize[] - */ - protected $_authorizeObjects = array(); - -/** - * The name of an optional view element to render when an Ajax request is made - * with an invalid or expired session - * - * @var string - */ - public $ajaxLogin = null; - -/** - * Settings to use when Auth needs to do a flash message with SessionComponent::setFlash(). - * Available keys are: - * - * - `element` - The element to use, defaults to 'default'. - * - `key` - The key to use, defaults to 'auth' - * - `params` - The array of additional params to use, defaults to array() - * - * @var array - */ - public $flash = array( - 'element' => 'default', - 'key' => 'auth', - 'params' => array() - ); - -/** - * The session key name where the record of the current user is stored. Default - * key is "Auth.User". If you are using only stateless authenticators set this - * to false to ensure session is not started. - * - * @var string - */ - public static $sessionKey = 'Auth.User'; - -/** - * The current user, used for stateless authentication when - * sessions are not available. - * - * @var array - */ - protected static $_user = array(); - -/** - * A URL (defined as a string or array) to the controller action that handles - * logins. Defaults to `/users/login`. - * - * @var mixed - */ - public $loginAction = array( - 'controller' => 'users', - 'action' => 'login', - 'plugin' => null - ); - -/** - * Normally, if a user is redirected to the $loginAction page, the location they - * were redirected from will be stored in the session so that they can be - * redirected back after a successful login. If this session value is not - * set, redirectUrl() method will return the URL specified in $loginRedirect. - * - * @var mixed - * @link https://book.cakephp.org/2.0/en/core-libraries/components/authentication.html#AuthComponent::$loginRedirect - */ - public $loginRedirect = null; - -/** - * The default action to redirect to after the user is logged out. While AuthComponent does - * not handle post-logout redirection, a redirect URL will be returned from AuthComponent::logout(). - * Defaults to AuthComponent::$loginAction. - * - * @var mixed - * @see AuthComponent::$loginAction - * @see AuthComponent::logout() - */ - public $logoutRedirect = null; - -/** - * Error to display when user attempts to access an object or action to which they do not have - * access. - * - * @var string|bool - * @link https://book.cakephp.org/2.0/en/core-libraries/components/authentication.html#AuthComponent::$authError - */ - public $authError = null; - -/** - * Controls handling of unauthorized access. - * - For default value `true` unauthorized user is redirected to the referrer URL - * or AuthComponent::$loginRedirect or '/'. - * - If set to a string or array the value is used as a URL to redirect to. - * - If set to false a ForbiddenException exception is thrown instead of redirecting. - * - * @var mixed - */ - public $unauthorizedRedirect = true; - -/** - * Controller actions for which user validation is not required. - * - * @var array - * @see AuthComponent::allow() - */ - public $allowedActions = array(); - -/** - * Request object - * - * @var CakeRequest - */ - public $request; - -/** - * Response object - * - * @var CakeResponse - */ - public $response; - -/** - * Method list for bound controller. - * - * @var array - */ - protected $_methods = array(); - -/** - * Initializes AuthComponent for use in the controller. - * - * @param Controller $controller A reference to the instantiating controller object - * @return void - */ - public function initialize(Controller $controller) { - $this->request = $controller->request; - $this->response = $controller->response; - $this->_methods = $controller->methods; - - if (Configure::read('debug') > 0) { - Debugger::checkSecurityKeys(); - } - } - -/** - * Main execution method. Handles redirecting of invalid users, and processing - * of login form data. - * - * @param Controller $controller A reference to the instantiating controller object - * @return bool - */ - public function startup(Controller $controller) { - $methods = array_flip(array_map('strtolower', $controller->methods)); - $action = strtolower($controller->request->params['action']); - - $isMissingAction = ( - $controller->scaffold === false && - !isset($methods[$action]) - ); - - if ($isMissingAction) { - return true; - } - - if (!$this->_setDefaults()) { - return false; - } - - if ($this->_isAllowed($controller)) { - $this->_getUser(); - return true; - } - - if (!$this->_getUser()) { - return $this->_unauthenticated($controller); - } - - if ($this->_isLoginAction($controller) || - empty($this->authorize) || - $this->isAuthorized($this->user()) - ) { - return true; - } - - return $this->_unauthorized($controller); - } - -/** - * Checks whether current action is accessible without authentication. - * - * @param Controller $controller A reference to the instantiating controller object - * @return bool True if action is accessible without authentication else false - */ - protected function _isAllowed(Controller $controller) { - $action = strtolower($controller->request->params['action']); - if (in_array($action, array_map('strtolower', $this->allowedActions))) { - return true; - } - return false; - } - -/** - * Handles unauthenticated access attempt. First the `unathenticated()` method - * of the last authenticator in the chain will be called. The authenticator can - * handle sending response or redirection as appropriate and return `true` to - * indicate no furthur action is necessary. If authenticator returns null this - * method redirects user to login action. If it's an ajax request and - * $ajaxLogin is specified that element is rendered else a 403 http status code - * is returned. - * - * @param Controller $controller A reference to the controller object. - * @return bool True if current action is login action else false. - */ - protected function _unauthenticated(Controller $controller) { - if (empty($this->_authenticateObjects)) { - $this->constructAuthenticate(); - } - $auth = $this->_authenticateObjects[count($this->_authenticateObjects) - 1]; - if ($auth->unauthenticated($this->request, $this->response)) { - return false; - } - - if ($this->_isLoginAction($controller)) { - if (empty($controller->request->data)) { - if (!$this->Session->check('Auth.redirect') && env('HTTP_REFERER')) { - $this->Session->write('Auth.redirect', $controller->referer(null, true)); - } - } - return true; - } - - if (!$controller->request->is('ajax') && !$controller->request->is('json')) { - $this->flash($this->authError); - $this->Session->write('Auth.redirect', $controller->request->here(false)); - $controller->redirect($this->loginAction); - return false; - } - if (!empty($this->ajaxLogin)) { - $controller->response->statusCode(403); - $controller->viewPath = 'Elements'; - $response = $controller->render($this->ajaxLogin, $this->RequestHandler->ajaxLayout); - $response->send(); - $this->_stop(); - return false; - } - $controller->response->statusCode(403); - $controller->response->send(); - $this->_stop(); - return false; - } - -/** - * Normalizes $loginAction and checks if current request URL is same as login action. - * - * @param Controller $controller A reference to the controller object. - * @return bool True if current action is login action else false. - */ - protected function _isLoginAction(Controller $controller) { - $url = ''; - if (isset($controller->request->url)) { - $url = $controller->request->url; - } - $url = Router::normalize($url); - $loginAction = Router::normalize($this->loginAction); - - return $loginAction === $url; - } - -/** - * Handle unauthorized access attempt - * - * @param Controller $controller A reference to the controller object - * @return bool Returns false - * @throws ForbiddenException - * @see AuthComponent::$unauthorizedRedirect - */ - protected function _unauthorized(Controller $controller) { - if ($this->unauthorizedRedirect === false) { - throw new ForbiddenException($this->authError); - } - - $this->flash($this->authError); - if ($this->unauthorizedRedirect === true) { - $default = '/'; - if (!empty($this->loginRedirect)) { - $default = $this->loginRedirect; - } - $url = $controller->referer($default, true); - } else { - $url = $this->unauthorizedRedirect; - } - $controller->redirect($url); - return false; - } - -/** - * Attempts to introspect the correct values for object properties. - * - * @return bool True - */ - protected function _setDefaults() { - $defaults = array( - 'logoutRedirect' => $this->loginAction, - 'authError' => __d('cake', 'You are not authorized to access that location.') - ); - foreach ($defaults as $key => $value) { - if (!isset($this->{$key}) || $this->{$key} === true) { - $this->{$key} = $value; - } - } - return true; - } - -/** - * Check if the provided user is authorized for the request. - * - * Uses the configured Authorization adapters to check whether or not a user is authorized. - * Each adapter will be checked in sequence, if any of them return true, then the user will - * be authorized for the request. - * - * @param array|null $user The user to check the authorization of. If empty the user in the session will be used. - * @param CakeRequest|null $request The request to authenticate for. If empty, the current request will be used. - * @return bool True if $user is authorized, otherwise false - */ - public function isAuthorized($user = null, CakeRequest $request = null) { - if (empty($user) && !$this->user()) { - return false; - } - if (empty($user)) { - $user = $this->user(); - } - if (empty($request)) { - $request = $this->request; - } - if (empty($this->_authorizeObjects)) { - $this->constructAuthorize(); - } - foreach ($this->_authorizeObjects as $authorizer) { - if ($authorizer->authorize($user, $request) === true) { - return true; - } - } - return false; - } - -/** - * Loads the authorization objects configured. - * - * @return mixed Either null when authorize is empty, or the loaded authorization objects. - * @throws CakeException - */ - public function constructAuthorize() { - if (empty($this->authorize)) { - return null; - } - $this->_authorizeObjects = array(); - $config = Hash::normalize((array)$this->authorize); - $global = array(); - if (isset($config[AuthComponent::ALL])) { - $global = $config[AuthComponent::ALL]; - unset($config[AuthComponent::ALL]); - } - foreach ($config as $class => $settings) { - list($plugin, $class) = pluginSplit($class, true); - $className = $class . 'Authorize'; - App::uses($className, $plugin . 'Controller/Component/Auth'); - if (!class_exists($className)) { - throw new CakeException(__d('cake_dev', 'Authorization adapter "%s" was not found.', $class)); - } - if (!method_exists($className, 'authorize')) { - throw new CakeException(__d('cake_dev', 'Authorization objects must implement an %s method.', 'authorize()')); - } - $settings = array_merge($global, (array)$settings); - $this->_authorizeObjects[] = new $className($this->_Collection, $settings); - } - return $this->_authorizeObjects; - } - -/** - * Takes a list of actions in the current controller for which authentication is not required, or - * no parameters to allow all actions. - * - * You can use allow with either an array, or var args. - * - * `$this->Auth->allow(array('edit', 'add'));` or - * `$this->Auth->allow('edit', 'add');` or - * `$this->Auth->allow();` to allow all actions - * - * @param string|array|null $action Controller action name or array of actions - * @return void - * @link https://book.cakephp.org/2.0/en/core-libraries/components/authentication.html#making-actions-public - */ - public function allow($action = null) { - $args = func_get_args(); - if (empty($args) || $action === null) { - $this->allowedActions = $this->_methods; - return; - } - if (isset($args[0]) && is_array($args[0])) { - $args = $args[0]; - } - $this->allowedActions = array_merge($this->allowedActions, $args); - } - -/** - * Removes items from the list of allowed/no authentication required actions. - * - * You can use deny with either an array, or var args. - * - * `$this->Auth->deny(array('edit', 'add'));` or - * `$this->Auth->deny('edit', 'add');` or - * `$this->Auth->deny();` to remove all items from the allowed list - * - * @param string|array|null $action Controller action name or array of actions - * @return void - * @see AuthComponent::allow() - * @link https://book.cakephp.org/2.0/en/core-libraries/components/authentication.html#making-actions-require-authorization - */ - public function deny($action = null) { - $args = func_get_args(); - if (empty($args) || $action === null) { - $this->allowedActions = array(); - return; - } - if (isset($args[0]) && is_array($args[0])) { - $args = $args[0]; - } - foreach ($args as $arg) { - $i = array_search($arg, $this->allowedActions); - if (is_int($i)) { - unset($this->allowedActions[$i]); - } - } - $this->allowedActions = array_values($this->allowedActions); - } - -/** - * Maps action names to CRUD operations. - * - * Used for controller-based authentication. Make sure - * to configure the authorize property before calling this method. As it delegates $map to all the - * attached authorize objects. - * - * @param array $map Actions to map - * @return array - * @see BaseAuthorize::mapActions() - * @link https://book.cakephp.org/2.0/en/core-libraries/components/authentication.html#mapping-actions-when-using-crudauthorize - * @deprecated 3.0.0 Map actions using `actionMap` config key on authorize objects instead - */ - public function mapActions($map = array()) { - if (empty($this->_authorizeObjects)) { - $this->constructAuthorize(); - } - $mappedActions = array(); - foreach ($this->_authorizeObjects as $auth) { - $mappedActions = Hash::merge($mappedActions, $auth->mapActions($map)); - } - if (empty($map)) { - return $mappedActions; - } - - return array(); - } - -/** - * Log a user in. - * - * If a $user is provided that data will be stored as the logged in user. If `$user` is empty or not - * specified, the request will be used to identify a user. If the identification was successful, - * the user record is written to the session key specified in AuthComponent::$sessionKey. Logging in - * will also change the session id in order to help mitigate session replays. - * - * @param array|null $user Either an array of user data, or null to identify a user using the current request. - * @return bool True on login success, false on failure - * @link https://book.cakephp.org/2.0/en/core-libraries/components/authentication.html#identifying-users-and-logging-them-in - */ - public function login($user = null) { - $this->_setDefaults(); - - if (empty($user)) { - $user = $this->identify($this->request, $this->response); - } - if ($user) { - if (static::$sessionKey) { - $this->Session->renew(); - $this->Session->write(static::$sessionKey, $user); - } else { - static::$_user = $user; - } - $event = new CakeEvent('Auth.afterIdentify', $this, array('user' => $user)); - $this->_Collection->getController()->getEventManager()->dispatch($event); - } - return (bool)$this->user(); - } - -/** - * Log a user out. - * - * Returns the logout action to redirect to. Triggers the logout() method of - * all the authenticate objects, so they can perform custom logout logic. - * AuthComponent will remove the session data, so there is no need to do that - * in an authentication object. Logging out will also renew the session id. - * This helps mitigate issues with session replays. - * - * @return string AuthComponent::$logoutRedirect - * @see AuthComponent::$logoutRedirect - * @link https://book.cakephp.org/2.0/en/core-libraries/components/authentication.html#logging-users-out - */ - public function logout() { - $this->_setDefaults(); - if (empty($this->_authenticateObjects)) { - $this->constructAuthenticate(); - } - $user = $this->user(); - foreach ($this->_authenticateObjects as $auth) { - $auth->logout($user); - } - static::$_user = array(); - $this->Session->delete(static::$sessionKey); - $this->Session->delete('Auth.redirect'); - $this->Session->renew(); - return Router::normalize($this->logoutRedirect); - } - -/** - * Get the current user. - * - * Will prefer the static user cache over sessions. The static user - * cache is primarily used for stateless authentication. For stateful authentication, - * cookies + sessions will be used. - * - * @param string|null $key field to retrieve. Leave null to get entire User record - * @return mixed|null User record. or null if no user is logged in. - * @link https://book.cakephp.org/2.0/en/core-libraries/components/authentication.html#accessing-the-logged-in-user - */ - public static function user($key = null) { - if (!empty(static::$_user)) { - $user = static::$_user; - } elseif (static::$sessionKey && CakeSession::check(static::$sessionKey)) { - $user = CakeSession::read(static::$sessionKey); - } else { - return null; - } - if ($key === null) { - return $user; - } - return Hash::get($user, $key); - } - -/** - * Similar to AuthComponent::user() except if the session user cannot be found, connected authentication - * objects will have their getUser() methods called. This lets stateless authentication methods function correctly. - * - * @return bool True if a user can be found, false if one cannot. - */ - protected function _getUser() { - $user = $this->user(); - if ($user) { - $this->Session->delete('Auth.redirect'); - return true; - } - - if (empty($this->_authenticateObjects)) { - $this->constructAuthenticate(); - } - foreach ($this->_authenticateObjects as $auth) { - $result = $auth->getUser($this->request); - if (!empty($result) && is_array($result)) { - static::$_user = $result; - return true; - } - } - - return false; - } - -/** - * Backwards compatible alias for AuthComponent::redirectUrl(). - * - * @param string|array|null $url Optional URL to write as the login redirect URL. - * @return string Redirect URL - * @deprecated 3.0.0 Since 2.3.0, use AuthComponent::redirectUrl() instead - */ - public function redirect($url = null) { - return $this->redirectUrl($url); - } - -/** - * Get the URL a user should be redirected to upon login. - * - * Pass a URL in to set the destination a user should be redirected to upon - * logging in. - * - * If no parameter is passed, gets the authentication redirect URL. The URL - * returned is as per following rules: - * - * - Returns the normalized URL from session Auth.redirect value if it is - * present and for the same domain the current app is running on. - * - If there is no session value and there is a $loginRedirect, the $loginRedirect - * value is returned. - * - If there is no session and no $loginRedirect, / is returned. - * - * @param string|array|null $url Optional URL to write as the login redirect URL. - * @return string Redirect URL - */ - public function redirectUrl($url = null) { - if ($url !== null) { - $redir = $url; - $this->Session->write('Auth.redirect', $redir); - } elseif ($this->Session->check('Auth.redirect')) { - $redir = $this->Session->read('Auth.redirect'); - $this->Session->delete('Auth.redirect'); - - if (Router::normalize($redir) === Router::normalize($this->loginAction)) { - $redir = $this->loginRedirect ?: '/'; - } - } elseif ($this->loginRedirect) { - $redir = $this->loginRedirect; - } else { - $redir = '/'; - } - if (is_array($redir)) { - return Router::url($redir + array('base' => false)); - } - return $redir; - } - -/** - * Use the configured authentication adapters, and attempt to identify the user - * by credentials contained in $request. - * - * @param CakeRequest $request The request that contains authentication data. - * @param CakeResponse $response The response - * @return array|bool User record data, or false, if the user could not be identified. - */ - public function identify(CakeRequest $request, CakeResponse $response) { - if (empty($this->_authenticateObjects)) { - $this->constructAuthenticate(); - } - foreach ($this->_authenticateObjects as $auth) { - $result = $auth->authenticate($request, $response); - if (!empty($result) && is_array($result)) { - return $result; - } - } - return false; - } - -/** - * Loads the configured authentication objects. - * - * @return mixed Either null on empty authenticate value, or an array of loaded objects. - * @throws CakeException - */ - public function constructAuthenticate() { - if (empty($this->authenticate)) { - return null; - } - $this->_authenticateObjects = array(); - $config = Hash::normalize((array)$this->authenticate); - $global = array(); - if (isset($config[AuthComponent::ALL])) { - $global = $config[AuthComponent::ALL]; - unset($config[AuthComponent::ALL]); - } - foreach ($config as $class => $settings) { - if (!empty($settings['className'])) { - $class = $settings['className']; - unset($settings['className']); - } - list($plugin, $class) = pluginSplit($class, true); - $className = $class . 'Authenticate'; - App::uses($className, $plugin . 'Controller/Component/Auth'); - if (!class_exists($className)) { - throw new CakeException(__d('cake_dev', 'Authentication adapter "%s" was not found.', $class)); - } - if (!method_exists($className, 'authenticate')) { - throw new CakeException(__d('cake_dev', 'Authentication objects must implement an %s method.', 'authenticate()')); - } - $settings = array_merge($global, (array)$settings); - $auth = new $className($this->_Collection, $settings); - $this->_Collection->getController()->getEventManager()->attach($auth); - $this->_authenticateObjects[] = $auth; - } - return $this->_authenticateObjects; - } - -/** - * Hash a password with the application's salt value (as defined with Configure::write('Security.salt'); - * - * This method is intended as a convenience wrapper for Security::hash(). If you want to use - * a hashing/encryption system not supported by that method, do not use this method. - * - * @param string $password Password to hash - * @return string Hashed password - * @deprecated 3.0.0 Since 2.4. Use Security::hash() directly or a password hasher object. - */ - public static function password($password) { - return Security::hash($password, null, true); - } - -/** - * Check whether or not the current user has data in the session, and is considered logged in. - * - * @return bool true if the user is logged in, false otherwise - * @deprecated 3.0.0 Since 2.5. Use AuthComponent::user() directly. - */ - public function loggedIn() { - return (bool)$this->user(); - } - -/** - * Set a flash message. Uses the Session component, and values from AuthComponent::$flash. - * - * @param string $message The message to set. - * @return void - */ - public function flash($message) { - if ($message === false) { - return; - } - $this->Flash->set($message, $this->flash); - } +class AuthComponent extends Component +{ + + /** + * Constant for 'all' + * + * @var string + */ + const ALL = 'all'; + /** + * The session key name where the record of the current user is stored. Default + * key is "Auth.User". If you are using only stateless authenticators set this + * to false to ensure session is not started. + * + * @var string + */ + public static $sessionKey = 'Auth.User'; + /** + * The current user, used for stateless authentication when + * sessions are not available. + * + * @var array + */ + protected static $_user = []; + /** + * Other components utilized by AuthComponent + * + * @var array + */ + public $components = ['Session', 'Flash', 'RequestHandler']; + /** + * An array of authentication objects to use for authenticating users. You can configure + * multiple adapters and they will be checked sequentially when users are identified. + * + * ``` + * $this->Auth->authenticate = array( + * 'Form' => array( + * 'userModel' => 'Users.User' + * ) + * ); + * ``` + * + * Using the class name without 'Authenticate' as the key, you can pass in an array of settings for each + * authentication object. Additionally you can define settings that should be set to all authentications objects + * using the 'all' key: + * + * ``` + * $this->Auth->authenticate = array( + * 'all' => array( + * 'userModel' => 'Users.User', + * 'scope' => array('User.active' => 1) + * ), + * 'Form', + * 'Basic' + * ); + * ``` + * + * You can also use AuthComponent::ALL instead of the string 'all'. + * + * @var array + * @link https://book.cakephp.org/2.0/en/core-libraries/components/authentication.html + */ + public $authenticate = ['Form']; + /** + * An array of authorization objects to use for authorizing users. You can configure + * multiple adapters and they will be checked sequentially when authorization checks are done. + * + * ``` + * $this->Auth->authorize = array( + * 'Crud' => array( + * 'actionPath' => 'controllers/' + * ) + * ); + * ``` + * + * Using the class name without 'Authorize' as the key, you can pass in an array of settings for each + * authorization object. Additionally you can define settings that should be set to all authorization objects + * using the 'all' key: + * + * ``` + * $this->Auth->authorize = array( + * 'all' => array( + * 'actionPath' => 'controllers/' + * ), + * 'Crud', + * 'CustomAuth' + * ); + * ``` + * + * You can also use AuthComponent::ALL instead of the string 'all' + * + * @var mixed + * @link https://book.cakephp.org/2.0/en/core-libraries/components/authentication.html#authorization + */ + public $authorize = false; + /** + * The name of an optional view element to render when an Ajax request is made + * with an invalid or expired session + * + * @var string + */ + public $ajaxLogin = null; + + /** + * Settings to use when Auth needs to do a flash message with SessionComponent::setFlash(). + * Available keys are: + * + * - `element` - The element to use, defaults to 'default'. + * - `key` - The key to use, defaults to 'auth' + * - `params` - The array of additional params to use, defaults to array() + * + * @var array + */ + public $flash = [ + 'element' => 'default', + 'key' => 'auth', + 'params' => [] + ]; + /** + * A URL (defined as a string or array) to the controller action that handles + * logins. Defaults to `/users/login`. + * + * @var mixed + */ + public $loginAction = [ + 'controller' => 'users', + 'action' => 'login', + 'plugin' => null + ]; + /** + * Normally, if a user is redirected to the $loginAction page, the location they + * were redirected from will be stored in the session so that they can be + * redirected back after a successful login. If this session value is not + * set, redirectUrl() method will return the URL specified in $loginRedirect. + * + * @var mixed + * @link https://book.cakephp.org/2.0/en/core-libraries/components/authentication.html#AuthComponent::$loginRedirect + */ + public $loginRedirect = null; + /** + * The default action to redirect to after the user is logged out. While AuthComponent does + * not handle post-logout redirection, a redirect URL will be returned from AuthComponent::logout(). + * Defaults to AuthComponent::$loginAction. + * + * @var mixed + * @see AuthComponent::$loginAction + * @see AuthComponent::logout() + */ + public $logoutRedirect = null; + /** + * Error to display when user attempts to access an object or action to which they do not have + * access. + * + * @var string|bool + * @link https://book.cakephp.org/2.0/en/core-libraries/components/authentication.html#AuthComponent::$authError + */ + public $authError = null; + /** + * Controls handling of unauthorized access. + * - For default value `true` unauthorized user is redirected to the referrer URL + * or AuthComponent::$loginRedirect or '/'. + * - If set to a string or array the value is used as a URL to redirect to. + * - If set to false a ForbiddenException exception is thrown instead of redirecting. + * + * @var mixed + */ + public $unauthorizedRedirect = true; + /** + * Controller actions for which user validation is not required. + * + * @var array + * @see AuthComponent::allow() + */ + public $allowedActions = []; + /** + * Request object + * + * @var CakeRequest + */ + public $request; + /** + * Response object + * + * @var CakeResponse + */ + public $response; + /** + * Objects that will be used for authentication checks. + * + * @var BaseAuthenticate[] + */ + protected $_authenticateObjects = []; + /** + * Objects that will be used for authorization checks. + * + * @var BaseAuthorize[] + */ + protected $_authorizeObjects = []; + /** + * Method list for bound controller. + * + * @var array + */ + protected $_methods = []; + + /** + * Hash a password with the application's salt value (as defined with Configure::write('Security.salt'); + * + * This method is intended as a convenience wrapper for Security::hash(). If you want to use + * a hashing/encryption system not supported by that method, do not use this method. + * + * @param string $password Password to hash + * @return string Hashed password + * @deprecated 3.0.0 Since 2.4. Use Security::hash() directly or a password hasher object. + */ + public static function password($password) + { + return Security::hash($password, null, true); + } + + /** + * Initializes AuthComponent for use in the controller. + * + * @param Controller $controller A reference to the instantiating controller object + * @return void + */ + public function initialize(Controller $controller) + { + $this->request = $controller->request; + $this->response = $controller->response; + $this->_methods = $controller->methods; + + if (Configure::read('debug') > 0) { + Debugger::checkSecurityKeys(); + } + } + + /** + * Main execution method. Handles redirecting of invalid users, and processing + * of login form data. + * + * @param Controller $controller A reference to the instantiating controller object + * @return bool + */ + public function startup(Controller $controller) + { + $methods = array_flip(array_map('strtolower', $controller->methods)); + $action = strtolower($controller->request->params['action']); + + $isMissingAction = ( + $controller->scaffold === false && + !isset($methods[$action]) + ); + + if ($isMissingAction) { + return true; + } + + if (!$this->_setDefaults()) { + return false; + } + + if ($this->_isAllowed($controller)) { + $this->_getUser(); + return true; + } + + if (!$this->_getUser()) { + return $this->_unauthenticated($controller); + } + + if ($this->_isLoginAction($controller) || + empty($this->authorize) || + $this->isAuthorized($this->user()) + ) { + return true; + } + + return $this->_unauthorized($controller); + } + + /** + * Attempts to introspect the correct values for object properties. + * + * @return bool True + */ + protected function _setDefaults() + { + $defaults = [ + 'logoutRedirect' => $this->loginAction, + 'authError' => __d('cake', 'You are not authorized to access that location.') + ]; + foreach ($defaults as $key => $value) { + if (!isset($this->{$key}) || $this->{$key} === true) { + $this->{$key} = $value; + } + } + return true; + } + + /** + * Checks whether current action is accessible without authentication. + * + * @param Controller $controller A reference to the instantiating controller object + * @return bool True if action is accessible without authentication else false + */ + protected function _isAllowed(Controller $controller) + { + $action = strtolower($controller->request->params['action']); + if (in_array($action, array_map('strtolower', $this->allowedActions))) { + return true; + } + return false; + } + + /** + * Similar to AuthComponent::user() except if the session user cannot be found, connected authentication + * objects will have their getUser() methods called. This lets stateless authentication methods function correctly. + * + * @return bool True if a user can be found, false if one cannot. + */ + protected function _getUser() + { + $user = $this->user(); + if ($user) { + $this->Session->delete('Auth.redirect'); + return true; + } + + if (empty($this->_authenticateObjects)) { + $this->constructAuthenticate(); + } + foreach ($this->_authenticateObjects as $auth) { + $result = $auth->getUser($this->request); + if (!empty($result) && is_array($result)) { + static::$_user = $result; + return true; + } + } + + return false; + } + + /** + * Get the current user. + * + * Will prefer the static user cache over sessions. The static user + * cache is primarily used for stateless authentication. For stateful authentication, + * cookies + sessions will be used. + * + * @param string|null $key field to retrieve. Leave null to get entire User record + * @return mixed|null User record. or null if no user is logged in. + * @link https://book.cakephp.org/2.0/en/core-libraries/components/authentication.html#accessing-the-logged-in-user + */ + public static function user($key = null) + { + if (!empty(static::$_user)) { + $user = static::$_user; + } else if (static::$sessionKey && CakeSession::check(static::$sessionKey)) { + $user = CakeSession::read(static::$sessionKey); + } else { + return null; + } + if ($key === null) { + return $user; + } + return Hash::get($user, $key); + } + + /** + * Loads the configured authentication objects. + * + * @return mixed Either null on empty authenticate value, or an array of loaded objects. + * @throws CakeException + */ + public function constructAuthenticate() + { + if (empty($this->authenticate)) { + return null; + } + $this->_authenticateObjects = []; + $config = Hash::normalize((array)$this->authenticate); + $global = []; + if (isset($config[AuthComponent::ALL])) { + $global = $config[AuthComponent::ALL]; + unset($config[AuthComponent::ALL]); + } + foreach ($config as $class => $settings) { + if (!empty($settings['className'])) { + $class = $settings['className']; + unset($settings['className']); + } + list($plugin, $class) = pluginSplit($class, true); + $className = $class . 'Authenticate'; + App::uses($className, $plugin . 'Controller/Component/Auth'); + if (!class_exists($className)) { + throw new CakeException(__d('cake_dev', 'Authentication adapter "%s" was not found.', $class)); + } + if (!method_exists($className, 'authenticate')) { + throw new CakeException(__d('cake_dev', 'Authentication objects must implement an %s method.', 'authenticate()')); + } + $settings = array_merge($global, (array)$settings); + $auth = new $className($this->_Collection, $settings); + $this->_Collection->getController()->getEventManager()->attach($auth); + $this->_authenticateObjects[] = $auth; + } + return $this->_authenticateObjects; + } + + /** + * Handles unauthenticated access attempt. First the `unathenticated()` method + * of the last authenticator in the chain will be called. The authenticator can + * handle sending response or redirection as appropriate and return `true` to + * indicate no furthur action is necessary. If authenticator returns null this + * method redirects user to login action. If it's an ajax request and + * $ajaxLogin is specified that element is rendered else a 403 http status code + * is returned. + * + * @param Controller $controller A reference to the controller object. + * @return bool True if current action is login action else false. + */ + protected function _unauthenticated(Controller $controller) + { + if (empty($this->_authenticateObjects)) { + $this->constructAuthenticate(); + } + $auth = $this->_authenticateObjects[count($this->_authenticateObjects) - 1]; + if ($auth->unauthenticated($this->request, $this->response)) { + return false; + } + + if ($this->_isLoginAction($controller)) { + if (empty($controller->request->data)) { + if (!$this->Session->check('Auth.redirect') && env('HTTP_REFERER')) { + $this->Session->write('Auth.redirect', $controller->referer(null, true)); + } + } + return true; + } + + if (!$controller->request->is('ajax') && !$controller->request->is('json')) { + $this->flash($this->authError); + $this->Session->write('Auth.redirect', $controller->request->here(false)); + $controller->redirect($this->loginAction); + return false; + } + if (!empty($this->ajaxLogin)) { + $controller->response->statusCode(403); + $controller->viewPath = 'Elements'; + $response = $controller->render($this->ajaxLogin, $this->RequestHandler->ajaxLayout); + $response->send(); + $this->_stop(); + return false; + } + $controller->response->statusCode(403); + $controller->response->send(); + $this->_stop(); + return false; + } + + /** + * Normalizes $loginAction and checks if current request URL is same as login action. + * + * @param Controller $controller A reference to the controller object. + * @return bool True if current action is login action else false. + */ + protected function _isLoginAction(Controller $controller) + { + $url = ''; + if (isset($controller->request->url)) { + $url = $controller->request->url; + } + $url = Router::normalize($url); + $loginAction = Router::normalize($this->loginAction); + + return $loginAction === $url; + } + + /** + * Set a flash message. Uses the Session component, and values from AuthComponent::$flash. + * + * @param string $message The message to set. + * @return void + */ + public function flash($message) + { + if ($message === false) { + return; + } + $this->Flash->set($message, $this->flash); + } + + /** + * Check if the provided user is authorized for the request. + * + * Uses the configured Authorization adapters to check whether or not a user is authorized. + * Each adapter will be checked in sequence, if any of them return true, then the user will + * be authorized for the request. + * + * @param array|null $user The user to check the authorization of. If empty the user in the session will be used. + * @param CakeRequest|null $request The request to authenticate for. If empty, the current request will be used. + * @return bool True if $user is authorized, otherwise false + */ + public function isAuthorized($user = null, CakeRequest $request = null) + { + if (empty($user) && !$this->user()) { + return false; + } + if (empty($user)) { + $user = $this->user(); + } + if (empty($request)) { + $request = $this->request; + } + if (empty($this->_authorizeObjects)) { + $this->constructAuthorize(); + } + foreach ($this->_authorizeObjects as $authorizer) { + if ($authorizer->authorize($user, $request) === true) { + return true; + } + } + return false; + } + + /** + * Loads the authorization objects configured. + * + * @return mixed Either null when authorize is empty, or the loaded authorization objects. + * @throws CakeException + */ + public function constructAuthorize() + { + if (empty($this->authorize)) { + return null; + } + $this->_authorizeObjects = []; + $config = Hash::normalize((array)$this->authorize); + $global = []; + if (isset($config[AuthComponent::ALL])) { + $global = $config[AuthComponent::ALL]; + unset($config[AuthComponent::ALL]); + } + foreach ($config as $class => $settings) { + list($plugin, $class) = pluginSplit($class, true); + $className = $class . 'Authorize'; + App::uses($className, $plugin . 'Controller/Component/Auth'); + if (!class_exists($className)) { + throw new CakeException(__d('cake_dev', 'Authorization adapter "%s" was not found.', $class)); + } + if (!method_exists($className, 'authorize')) { + throw new CakeException(__d('cake_dev', 'Authorization objects must implement an %s method.', 'authorize()')); + } + $settings = array_merge($global, (array)$settings); + $this->_authorizeObjects[] = new $className($this->_Collection, $settings); + } + return $this->_authorizeObjects; + } + + /** + * Handle unauthorized access attempt + * + * @param Controller $controller A reference to the controller object + * @return bool Returns false + * @throws ForbiddenException + * @see AuthComponent::$unauthorizedRedirect + */ + protected function _unauthorized(Controller $controller) + { + if ($this->unauthorizedRedirect === false) { + throw new ForbiddenException($this->authError); + } + + $this->flash($this->authError); + if ($this->unauthorizedRedirect === true) { + $default = '/'; + if (!empty($this->loginRedirect)) { + $default = $this->loginRedirect; + } + $url = $controller->referer($default, true); + } else { + $url = $this->unauthorizedRedirect; + } + $controller->redirect($url); + return false; + } + + /** + * Takes a list of actions in the current controller for which authentication is not required, or + * no parameters to allow all actions. + * + * You can use allow with either an array, or var args. + * + * `$this->Auth->allow(array('edit', 'add'));` or + * `$this->Auth->allow('edit', 'add');` or + * `$this->Auth->allow();` to allow all actions + * + * @param string|array|null $action Controller action name or array of actions + * @return void + * @link https://book.cakephp.org/2.0/en/core-libraries/components/authentication.html#making-actions-public + */ + public function allow($action = null) + { + $args = func_get_args(); + if (empty($args) || $action === null) { + $this->allowedActions = $this->_methods; + return; + } + if (isset($args[0]) && is_array($args[0])) { + $args = $args[0]; + } + $this->allowedActions = array_merge($this->allowedActions, $args); + } + + /** + * Removes items from the list of allowed/no authentication required actions. + * + * You can use deny with either an array, or var args. + * + * `$this->Auth->deny(array('edit', 'add'));` or + * `$this->Auth->deny('edit', 'add');` or + * `$this->Auth->deny();` to remove all items from the allowed list + * + * @param string|array|null $action Controller action name or array of actions + * @return void + * @see AuthComponent::allow() + * @link https://book.cakephp.org/2.0/en/core-libraries/components/authentication.html#making-actions-require-authorization + */ + public function deny($action = null) + { + $args = func_get_args(); + if (empty($args) || $action === null) { + $this->allowedActions = []; + return; + } + if (isset($args[0]) && is_array($args[0])) { + $args = $args[0]; + } + foreach ($args as $arg) { + $i = array_search($arg, $this->allowedActions); + if (is_int($i)) { + unset($this->allowedActions[$i]); + } + } + $this->allowedActions = array_values($this->allowedActions); + } + + /** + * Maps action names to CRUD operations. + * + * Used for controller-based authentication. Make sure + * to configure the authorize property before calling this method. As it delegates $map to all the + * attached authorize objects. + * + * @param array $map Actions to map + * @return array + * @see BaseAuthorize::mapActions() + * @link https://book.cakephp.org/2.0/en/core-libraries/components/authentication.html#mapping-actions-when-using-crudauthorize + * @deprecated 3.0.0 Map actions using `actionMap` config key on authorize objects instead + */ + public function mapActions($map = []) + { + if (empty($this->_authorizeObjects)) { + $this->constructAuthorize(); + } + $mappedActions = []; + foreach ($this->_authorizeObjects as $auth) { + $mappedActions = Hash::merge($mappedActions, $auth->mapActions($map)); + } + if (empty($map)) { + return $mappedActions; + } + + return []; + } + + /** + * Log a user in. + * + * If a $user is provided that data will be stored as the logged in user. If `$user` is empty or not + * specified, the request will be used to identify a user. If the identification was successful, + * the user record is written to the session key specified in AuthComponent::$sessionKey. Logging in + * will also change the session id in order to help mitigate session replays. + * + * @param array|null $user Either an array of user data, or null to identify a user using the current request. + * @return bool True on login success, false on failure + * @link https://book.cakephp.org/2.0/en/core-libraries/components/authentication.html#identifying-users-and-logging-them-in + */ + public function login($user = null) + { + $this->_setDefaults(); + + if (empty($user)) { + $user = $this->identify($this->request, $this->response); + } + if ($user) { + if (static::$sessionKey) { + $this->Session->renew(); + $this->Session->write(static::$sessionKey, $user); + } else { + static::$_user = $user; + } + $event = new CakeEvent('Auth.afterIdentify', $this, ['user' => $user]); + $this->_Collection->getController()->getEventManager()->dispatch($event); + } + return (bool)$this->user(); + } + + /** + * Use the configured authentication adapters, and attempt to identify the user + * by credentials contained in $request. + * + * @param CakeRequest $request The request that contains authentication data. + * @param CakeResponse $response The response + * @return array|bool User record data, or false, if the user could not be identified. + */ + public function identify(CakeRequest $request, CakeResponse $response) + { + if (empty($this->_authenticateObjects)) { + $this->constructAuthenticate(); + } + foreach ($this->_authenticateObjects as $auth) { + $result = $auth->authenticate($request, $response); + if (!empty($result) && is_array($result)) { + return $result; + } + } + return false; + } + + /** + * Log a user out. + * + * Returns the logout action to redirect to. Triggers the logout() method of + * all the authenticate objects, so they can perform custom logout logic. + * AuthComponent will remove the session data, so there is no need to do that + * in an authentication object. Logging out will also renew the session id. + * This helps mitigate issues with session replays. + * + * @return string AuthComponent::$logoutRedirect + * @see AuthComponent::$logoutRedirect + * @link https://book.cakephp.org/2.0/en/core-libraries/components/authentication.html#logging-users-out + */ + public function logout() + { + $this->_setDefaults(); + if (empty($this->_authenticateObjects)) { + $this->constructAuthenticate(); + } + $user = $this->user(); + foreach ($this->_authenticateObjects as $auth) { + $auth->logout($user); + } + static::$_user = []; + $this->Session->delete(static::$sessionKey); + $this->Session->delete('Auth.redirect'); + $this->Session->renew(); + return Router::normalize($this->logoutRedirect); + } + + /** + * Backwards compatible alias for AuthComponent::redirectUrl(). + * + * @param string|array|null $url Optional URL to write as the login redirect URL. + * @return string Redirect URL + * @deprecated 3.0.0 Since 2.3.0, use AuthComponent::redirectUrl() instead + */ + public function redirect($url = null) + { + return $this->redirectUrl($url); + } + + /** + * Get the URL a user should be redirected to upon login. + * + * Pass a URL in to set the destination a user should be redirected to upon + * logging in. + * + * If no parameter is passed, gets the authentication redirect URL. The URL + * returned is as per following rules: + * + * - Returns the normalized URL from session Auth.redirect value if it is + * present and for the same domain the current app is running on. + * - If there is no session value and there is a $loginRedirect, the $loginRedirect + * value is returned. + * - If there is no session and no $loginRedirect, / is returned. + * + * @param string|array|null $url Optional URL to write as the login redirect URL. + * @return string Redirect URL + */ + public function redirectUrl($url = null) + { + if ($url !== null) { + $redir = $url; + $this->Session->write('Auth.redirect', $redir); + } else if ($this->Session->check('Auth.redirect')) { + $redir = $this->Session->read('Auth.redirect'); + $this->Session->delete('Auth.redirect'); + + if (Router::normalize($redir) === Router::normalize($this->loginAction)) { + $redir = $this->loginRedirect ?: '/'; + } + } else if ($this->loginRedirect) { + $redir = $this->loginRedirect; + } else { + $redir = '/'; + } + if (is_array($redir)) { + return Router::url($redir + ['base' => false]); + } + return $redir; + } + + /** + * Check whether or not the current user has data in the session, and is considered logged in. + * + * @return bool true if the user is logged in, false otherwise + * @deprecated 3.0.0 Since 2.5. Use AuthComponent::user() directly. + */ + public function loggedIn() + { + return (bool)$this->user(); + } } diff --git a/lib/Cake/Controller/Component/CookieComponent.php b/lib/Cake/Controller/Component/CookieComponent.php index 7d07d869..08559a3c 100755 --- a/lib/Cake/Controller/Component/CookieComponent.php +++ b/lib/Cake/Controller/Component/CookieComponent.php @@ -28,507 +28,524 @@ * @package Cake.Controller.Component * @link https://book.cakephp.org/2.0/en/core-libraries/components/cookie.html */ -class CookieComponent extends Component { - -/** - * The name of the cookie. - * - * Overridden with the controller beforeFilter(); - * $this->Cookie->name = 'CookieName'; - * - * @var string - */ - public $name = 'CakeCookie'; - -/** - * The time a cookie will remain valid. - * - * Can be either integer Unix timestamp or a date string. - * - * Overridden with the controller beforeFilter(); - * $this->Cookie->time = '5 Days'; - * - * @var mixed - */ - public $time = null; - -/** - * Cookie path. - * - * Overridden with the controller beforeFilter(); - * $this->Cookie->path = '/'; - * - * The path on the server in which the cookie will be available on. - * If public $cookiePath is set to '/foo/', the cookie will only be available - * within the /foo/ directory and all sub-directories such as /foo/bar/ of domain. - * The default value is the entire domain. - * - * @var string - */ - public $path = '/'; - -/** - * Domain path. - * - * The domain that the cookie is available. - * - * Overridden with the controller beforeFilter(); - * $this->Cookie->domain = '.example.com'; - * - * To make the cookie available on all subdomains of example.com. - * Set $this->Cookie->domain = '.example.com'; in your controller beforeFilter - * - * @var string - */ - public $domain = ''; - -/** - * Secure HTTPS only cookie. - * - * Overridden with the controller beforeFilter(); - * $this->Cookie->secure = true; - * - * Indicates that the cookie should only be transmitted over a secure HTTPS connection. - * When set to true, the cookie will only be set if a secure connection exists. - * - * @var bool - */ - public $secure = false; - -/** - * Encryption key. - * - * Overridden with the controller beforeFilter(); - * $this->Cookie->key = 'SomeRandomString'; - * - * @var string - */ - public $key = null; - -/** - * HTTP only cookie - * - * Set to true to make HTTP only cookies. Cookies that are HTTP only - * are not accessible in JavaScript. - * - * @var bool - */ - public $httpOnly = false; - -/** - * Values stored in the cookie. - * - * Accessed in the controller using $this->Cookie->read('Name.key'); - * - * @see CookieComponent::read(); - * @var string - */ - protected $_values = array(); - -/** - * Type of encryption to use. - * - * Currently two methods are available: cipher and rijndael - * Defaults to Security::cipher(). Cipher is horribly insecure and only - * the default because of backwards compatibility. In new applications you should - * always change this to 'aes' or 'rijndael'. - * - * @var string - */ - protected $_type = 'cipher'; - -/** - * Used to reset cookie time if $expire is passed to CookieComponent::write() - * - * @var string - */ - protected $_reset = null; - -/** - * Expire time of the cookie - * - * This is controlled by CookieComponent::time; - * - * @var string - */ - protected $_expires = 0; - -/** - * A reference to the Controller's CakeResponse object - * - * @var CakeResponse - */ - protected $_response = null; - -/** - * Constructor - * - * @param ComponentCollection $collection A ComponentCollection for this component - * @param array $settings Array of settings. - */ - public function __construct(ComponentCollection $collection, $settings = array()) { - $this->key = Configure::read('Security.salt'); - parent::__construct($collection, $settings); - if (isset($this->time)) { - $this->_expire($this->time); - } - - $controller = $collection->getController(); - if ($controller && isset($controller->response)) { - $this->_response = $controller->response; - } else { - $this->_response = new CakeResponse(); - } - } - -/** - * Start CookieComponent for use in the controller - * - * @param Controller $controller Controller instance. - * @return void - */ - public function startup(Controller $controller) { - $this->_expire($this->time); - - $this->_values[$this->name] = array(); - } - -/** - * Write a value to the $_COOKIE[$key]; - * - * Optional [Name.], required key, optional $value, optional $encrypt, optional $expires - * $this->Cookie->write('[Name.]key, $value); - * - * By default all values are encrypted. - * You must pass $encrypt false to store values in clear test - * - * You must use this method before any output is sent to the browser. - * Failure to do so will result in header already sent errors. - * - * @param string|array $key Key for the value - * @param mixed $value Value - * @param bool $encrypt Set to true to encrypt value, false otherwise - * @param int|string $expires Can be either the number of seconds until a cookie - * expires, or a strtotime compatible time offset. - * @return void - * @link https://book.cakephp.org/2.0/en/core-libraries/components/cookie.html#CookieComponent::write - */ - public function write($key, $value = null, $encrypt = true, $expires = null) { - if (empty($this->_values[$this->name])) { - $this->read(); - } - - if ($encrypt === null) { - $encrypt = true; - } - $this->_encrypted = $encrypt; - $this->_expire($expires); - - if (!is_array($key)) { - $key = array($key => $value); - } - - foreach ($key as $name => $value) { - if (strpos($name, '.') !== false) { - $this->_values[$this->name] = Hash::insert($this->_values[$this->name], $name, $value); - list($name) = explode('.', $name, 2); - $value = $this->_values[$this->name][$name]; - } else { - $this->_values[$this->name][$name] = $value; - } - $this->_write('[' . $name . ']', $value); - } - $this->_encrypted = true; - } - -/** - * Read the value of the $_COOKIE[$key]; - * - * Optional [Name.], required key - * $this->Cookie->read(Name.key); - * - * @param string $key Key of the value to be obtained. If none specified, obtain map key => values - * @return string|array|null Value for specified key - * @link https://book.cakephp.org/2.0/en/core-libraries/components/cookie.html#CookieComponent::read - */ - public function read($key = null) { - if (empty($this->_values[$this->name]) && isset($_COOKIE[$this->name])) { - $this->_values[$this->name] = $this->_decrypt($_COOKIE[$this->name]); - } - if (empty($this->_values[$this->name])) { - $this->_values[$this->name] = array(); - } - if ($key === null) { - return $this->_values[$this->name]; - } - return Hash::get($this->_values[$this->name], $key); - } - -/** - * Returns true if given variable is set in cookie. - * - * @param string $key Variable name to check for - * @return bool True if variable is there - */ - public function check($key = null) { - if (empty($key)) { - return false; - } - return $this->read($key) !== null; - } - -/** - * Delete a cookie value - * - * Optional [Name.], required key - * $this->Cookie->delete('Name.key); - * - * You must use this method before any output is sent to the browser. - * Failure to do so will result in header already sent errors. - * - * This method will delete both the top level and 2nd level cookies set. - * For example assuming that $name = App, deleting `User` will delete - * both `App[User]` and any other cookie values like `App[User][email]` - * This is done to clean up cookie storage from before 2.4.3, where cookies - * were stored inconsistently. - * - * @param string $key Key of the value to be deleted - * @return void - * @link https://book.cakephp.org/2.0/en/core-libraries/components/cookie.html#CookieComponent::delete - */ - public function delete($key) { - if (empty($this->_values[$this->name])) { - $this->read(); - } - if (strpos($key, '.') === false) { - unset($this->_values[$this->name][$key]); - $this->_delete('[' . $key . ']'); - } else { - $this->_values[$this->name] = Hash::remove((array)$this->_values[$this->name], $key); - list($key) = explode('.', $key, 2); - if (isset($this->_values[$this->name][$key])) { - $value = $this->_values[$this->name][$key]; - $this->_write('[' . $key . ']', $value); - } - } - } - -/** - * Destroy current cookie - * - * You must use this method before any output is sent to the browser. - * Failure to do so will result in header already sent errors. - * - * @return void - * @link https://book.cakephp.org/2.0/en/core-libraries/components/cookie.html#CookieComponent::destroy - */ - public function destroy() { - if (isset($_COOKIE[$this->name])) { - $this->_values[$this->name] = $this->_decrypt($_COOKIE[$this->name]); - } - - foreach ($this->_values[$this->name] as $name => $value) { - $this->delete($name); - } - } - -/** - * Will allow overriding default encryption method. Use this method - * in ex: AppController::beforeFilter() before you have read or - * written any cookies. - * - * @param string $type Encryption method - * @return void - */ - public function type($type = 'cipher') { - $availableTypes = array( - 'cipher', - 'rijndael', - 'aes' - ); - if (!in_array($type, $availableTypes)) { - trigger_error(__d('cake_dev', 'You must use cipher, rijndael or aes for cookie encryption type'), E_USER_WARNING); - $type = 'cipher'; - } - $this->_type = $type; - } - -/** - * Set the expire time for a session variable. - * - * Creates a new expire time for a session variable. - * $expire can be either integer Unix timestamp or a date string. - * - * Used by write() - * CookieComponent::write(string, string, boolean, 8400); - * CookieComponent::write(string, string, boolean, '5 Days'); - * - * @param int|string $expires Can be either Unix timestamp, or date string - * @return int Unix timestamp - */ - protected function _expire($expires = null) { - if ($expires === null) { - return $this->_expires; - } - $this->_reset = $this->_expires; - if (!$expires) { - return $this->_expires = 0; - } - $now = new DateTime(); - - if (is_int($expires) || is_numeric($expires)) { - return $this->_expires = $now->format('U') + (int)$expires; - } - $now->modify($expires); - return $this->_expires = $now->format('U'); - } - -/** - * Set cookie - * - * @param string $name Name for cookie - * @param string $value Value for cookie - * @return void - */ - protected function _write($name, $value) { - $this->_response->cookie(array( - 'name' => $this->name . $name, - 'value' => $this->_encrypt($value), - 'expire' => $this->_expires, - 'path' => $this->path, - 'domain' => $this->domain, - 'secure' => $this->secure, - 'httpOnly' => $this->httpOnly - )); - - if (!empty($this->_reset)) { - $this->_expires = $this->_reset; - $this->_reset = null; - } - } - -/** - * Sets a cookie expire time to remove cookie value - * - * @param string $name Name of cookie - * @return void - */ - protected function _delete($name) { - $this->_response->cookie(array( - 'name' => $this->name . $name, - 'value' => '', - 'expire' => time() - 42000, - 'path' => $this->path, - 'domain' => $this->domain, - 'secure' => $this->secure, - 'httpOnly' => $this->httpOnly - )); - } - -/** - * Encrypts $value using public $type method in Security class - * - * @param string $value Value to encrypt - * @return string Encoded values - */ - protected function _encrypt($value) { - if (is_array($value)) { - $value = $this->_implode($value); - } - if (!$this->_encrypted) { - return $value; - } - $prefix = "Q2FrZQ==."; - if ($this->_type === 'rijndael') { - $cipher = Security::rijndael($value, $this->key, 'encrypt'); - } - if ($this->_type === 'cipher') { - $cipher = Security::cipher($value, $this->key); - } - if ($this->_type === 'aes') { - $cipher = Security::encrypt($value, $this->key); - } - return $prefix . base64_encode($cipher); - } - -/** - * Decrypts $value using public $type method in Security class - * - * @param array $values Values to decrypt - * @return array decrypted string - */ - protected function _decrypt($values) { - $decrypted = array(); - $type = $this->_type; - - foreach ((array)$values as $name => $value) { - if (is_array($value)) { - foreach ($value as $key => $val) { - $decrypted[$name][$key] = $this->_decode($val); - } - } else { - $decrypted[$name] = $this->_decode($value); - } - } - return $decrypted; - } - -/** - * Decodes and decrypts a single value. - * - * @param string $value The value to decode & decrypt. - * @return string|array Decoded value. - */ - protected function _decode($value) { - $prefix = 'Q2FrZQ==.'; - $pos = strpos($value, $prefix); - if ($pos === false) { - return $this->_explode($value); - } - $value = base64_decode(substr($value, strlen($prefix))); - if ($this->_type === 'rijndael') { - $plain = Security::rijndael($value, $this->key, 'decrypt'); - } - if ($this->_type === 'cipher') { - $plain = Security::cipher($value, $this->key); - } - if ($this->_type === 'aes') { - $plain = Security::decrypt($value, $this->key); - } - return $this->_explode($plain); - } - -/** - * Implode method to keep keys are multidimensional arrays - * - * @param array $array Map of key and values - * @return string A json encoded string. - */ - protected function _implode(array $array) { - return json_encode($array); - } - -/** - * Explode method to return array from string set in CookieComponent::_implode() - * Maintains reading backwards compatibility with 1.x CookieComponent::_implode(). - * - * @param string $string A string containing JSON encoded data, or a bare string. - * @return string|array Map of key and values - */ - protected function _explode($string) { - $first = substr($string, 0, 1); - if ($first === '{' || $first === '[') { - $ret = json_decode($string, true); - return ($ret !== null) ? $ret : $string; - } - $array = array(); - foreach (explode(',', $string) as $pair) { - $key = explode('|', $pair); - if (!isset($key[1])) { - return $key[0]; - } - $array[$key[0]] = $key[1]; - } - return $array; - } +class CookieComponent extends Component +{ + + /** + * The name of the cookie. + * + * Overridden with the controller beforeFilter(); + * $this->Cookie->name = 'CookieName'; + * + * @var string + */ + public $name = 'CakeCookie'; + + /** + * The time a cookie will remain valid. + * + * Can be either integer Unix timestamp or a date string. + * + * Overridden with the controller beforeFilter(); + * $this->Cookie->time = '5 Days'; + * + * @var mixed + */ + public $time = null; + + /** + * Cookie path. + * + * Overridden with the controller beforeFilter(); + * $this->Cookie->path = '/'; + * + * The path on the server in which the cookie will be available on. + * If public $cookiePath is set to '/foo/', the cookie will only be available + * within the /foo/ directory and all sub-directories such as /foo/bar/ of domain. + * The default value is the entire domain. + * + * @var string + */ + public $path = '/'; + + /** + * Domain path. + * + * The domain that the cookie is available. + * + * Overridden with the controller beforeFilter(); + * $this->Cookie->domain = '.example.com'; + * + * To make the cookie available on all subdomains of example.com. + * Set $this->Cookie->domain = '.example.com'; in your controller beforeFilter + * + * @var string + */ + public $domain = ''; + + /** + * Secure HTTPS only cookie. + * + * Overridden with the controller beforeFilter(); + * $this->Cookie->secure = true; + * + * Indicates that the cookie should only be transmitted over a secure HTTPS connection. + * When set to true, the cookie will only be set if a secure connection exists. + * + * @var bool + */ + public $secure = false; + + /** + * Encryption key. + * + * Overridden with the controller beforeFilter(); + * $this->Cookie->key = 'SomeRandomString'; + * + * @var string + */ + public $key = null; + + /** + * HTTP only cookie + * + * Set to true to make HTTP only cookies. Cookies that are HTTP only + * are not accessible in JavaScript. + * + * @var bool + */ + public $httpOnly = false; + + /** + * Values stored in the cookie. + * + * Accessed in the controller using $this->Cookie->read('Name.key'); + * + * @see CookieComponent::read(); + * @var string + */ + protected $_values = []; + + /** + * Type of encryption to use. + * + * Currently two methods are available: cipher and rijndael + * Defaults to Security::cipher(). Cipher is horribly insecure and only + * the default because of backwards compatibility. In new applications you should + * always change this to 'aes' or 'rijndael'. + * + * @var string + */ + protected $_type = 'cipher'; + + /** + * Used to reset cookie time if $expire is passed to CookieComponent::write() + * + * @var string + */ + protected $_reset = null; + + /** + * Expire time of the cookie + * + * This is controlled by CookieComponent::time; + * + * @var string + */ + protected $_expires = 0; + + /** + * A reference to the Controller's CakeResponse object + * + * @var CakeResponse + */ + protected $_response = null; + + /** + * Constructor + * + * @param ComponentCollection $collection A ComponentCollection for this component + * @param array $settings Array of settings. + */ + public function __construct(ComponentCollection $collection, $settings = []) + { + $this->key = Configure::read('Security.salt'); + parent::__construct($collection, $settings); + if (isset($this->time)) { + $this->_expire($this->time); + } + + $controller = $collection->getController(); + if ($controller && isset($controller->response)) { + $this->_response = $controller->response; + } else { + $this->_response = new CakeResponse(); + } + } + + /** + * Set the expire time for a session variable. + * + * Creates a new expire time for a session variable. + * $expire can be either integer Unix timestamp or a date string. + * + * Used by write() + * CookieComponent::write(string, string, boolean, 8400); + * CookieComponent::write(string, string, boolean, '5 Days'); + * + * @param int|string $expires Can be either Unix timestamp, or date string + * @return int Unix timestamp + */ + protected function _expire($expires = null) + { + if ($expires === null) { + return $this->_expires; + } + $this->_reset = $this->_expires; + if (!$expires) { + return $this->_expires = 0; + } + $now = new DateTime(); + + if (is_int($expires) || is_numeric($expires)) { + return $this->_expires = $now->format('U') + (int)$expires; + } + $now->modify($expires); + return $this->_expires = $now->format('U'); + } + + /** + * Start CookieComponent for use in the controller + * + * @param Controller $controller Controller instance. + * @return void + */ + public function startup(Controller $controller) + { + $this->_expire($this->time); + + $this->_values[$this->name] = []; + } + + /** + * Write a value to the $_COOKIE[$key]; + * + * Optional [Name.], required key, optional $value, optional $encrypt, optional $expires + * $this->Cookie->write('[Name.]key, $value); + * + * By default all values are encrypted. + * You must pass $encrypt false to store values in clear test + * + * You must use this method before any output is sent to the browser. + * Failure to do so will result in header already sent errors. + * + * @param string|array $key Key for the value + * @param mixed $value Value + * @param bool $encrypt Set to true to encrypt value, false otherwise + * @param int|string $expires Can be either the number of seconds until a cookie + * expires, or a strtotime compatible time offset. + * @return void + * @link https://book.cakephp.org/2.0/en/core-libraries/components/cookie.html#CookieComponent::write + */ + public function write($key, $value = null, $encrypt = true, $expires = null) + { + if (empty($this->_values[$this->name])) { + $this->read(); + } + + if ($encrypt === null) { + $encrypt = true; + } + $this->_encrypted = $encrypt; + $this->_expire($expires); + + if (!is_array($key)) { + $key = [$key => $value]; + } + + foreach ($key as $name => $value) { + if (strpos($name, '.') !== false) { + $this->_values[$this->name] = Hash::insert($this->_values[$this->name], $name, $value); + list($name) = explode('.', $name, 2); + $value = $this->_values[$this->name][$name]; + } else { + $this->_values[$this->name][$name] = $value; + } + $this->_write('[' . $name . ']', $value); + } + $this->_encrypted = true; + } + + /** + * Read the value of the $_COOKIE[$key]; + * + * Optional [Name.], required key + * $this->Cookie->read(Name.key); + * + * @param string $key Key of the value to be obtained. If none specified, obtain map key => values + * @return string|array|null Value for specified key + * @link https://book.cakephp.org/2.0/en/core-libraries/components/cookie.html#CookieComponent::read + */ + public function read($key = null) + { + if (empty($this->_values[$this->name]) && isset($_COOKIE[$this->name])) { + $this->_values[$this->name] = $this->_decrypt($_COOKIE[$this->name]); + } + if (empty($this->_values[$this->name])) { + $this->_values[$this->name] = []; + } + if ($key === null) { + return $this->_values[$this->name]; + } + return Hash::get($this->_values[$this->name], $key); + } + + /** + * Decrypts $value using public $type method in Security class + * + * @param array $values Values to decrypt + * @return array decrypted string + */ + protected function _decrypt($values) + { + $decrypted = []; + $type = $this->_type; + + foreach ((array)$values as $name => $value) { + if (is_array($value)) { + foreach ($value as $key => $val) { + $decrypted[$name][$key] = $this->_decode($val); + } + } else { + $decrypted[$name] = $this->_decode($value); + } + } + return $decrypted; + } + + /** + * Decodes and decrypts a single value. + * + * @param string $value The value to decode & decrypt. + * @return string|array Decoded value. + */ + protected function _decode($value) + { + $prefix = 'Q2FrZQ==.'; + $pos = strpos($value, $prefix); + if ($pos === false) { + return $this->_explode($value); + } + $value = base64_decode(substr($value, strlen($prefix))); + if ($this->_type === 'rijndael') { + $plain = Security::rijndael($value, $this->key, 'decrypt'); + } + if ($this->_type === 'cipher') { + $plain = Security::cipher($value, $this->key); + } + if ($this->_type === 'aes') { + $plain = Security::decrypt($value, $this->key); + } + return $this->_explode($plain); + } + + /** + * Explode method to return array from string set in CookieComponent::_implode() + * Maintains reading backwards compatibility with 1.x CookieComponent::_implode(). + * + * @param string $string A string containing JSON encoded data, or a bare string. + * @return string|array Map of key and values + */ + protected function _explode($string) + { + $first = substr($string, 0, 1); + if ($first === '{' || $first === '[') { + $ret = json_decode($string, true); + return ($ret !== null) ? $ret : $string; + } + $array = []; + foreach (explode(',', $string) as $pair) { + $key = explode('|', $pair); + if (!isset($key[1])) { + return $key[0]; + } + $array[$key[0]] = $key[1]; + } + return $array; + } + + /** + * Set cookie + * + * @param string $name Name for cookie + * @param string $value Value for cookie + * @return void + */ + protected function _write($name, $value) + { + $this->_response->cookie([ + 'name' => $this->name . $name, + 'value' => $this->_encrypt($value), + 'expire' => $this->_expires, + 'path' => $this->path, + 'domain' => $this->domain, + 'secure' => $this->secure, + 'httpOnly' => $this->httpOnly + ]); + + if (!empty($this->_reset)) { + $this->_expires = $this->_reset; + $this->_reset = null; + } + } + + /** + * Encrypts $value using public $type method in Security class + * + * @param string $value Value to encrypt + * @return string Encoded values + */ + protected function _encrypt($value) + { + if (is_array($value)) { + $value = $this->_implode($value); + } + if (!$this->_encrypted) { + return $value; + } + $prefix = "Q2FrZQ==."; + if ($this->_type === 'rijndael') { + $cipher = Security::rijndael($value, $this->key, 'encrypt'); + } + if ($this->_type === 'cipher') { + $cipher = Security::cipher($value, $this->key); + } + if ($this->_type === 'aes') { + $cipher = Security::encrypt($value, $this->key); + } + return $prefix . base64_encode($cipher); + } + + /** + * Implode method to keep keys are multidimensional arrays + * + * @param array $array Map of key and values + * @return string A json encoded string. + */ + protected function _implode(array $array) + { + return json_encode($array); + } + + /** + * Returns true if given variable is set in cookie. + * + * @param string $key Variable name to check for + * @return bool True if variable is there + */ + public function check($key = null) + { + if (empty($key)) { + return false; + } + return $this->read($key) !== null; + } + + /** + * Destroy current cookie + * + * You must use this method before any output is sent to the browser. + * Failure to do so will result in header already sent errors. + * + * @return void + * @link https://book.cakephp.org/2.0/en/core-libraries/components/cookie.html#CookieComponent::destroy + */ + public function destroy() + { + if (isset($_COOKIE[$this->name])) { + $this->_values[$this->name] = $this->_decrypt($_COOKIE[$this->name]); + } + + foreach ($this->_values[$this->name] as $name => $value) { + $this->delete($name); + } + } + + /** + * Delete a cookie value + * + * Optional [Name.], required key + * $this->Cookie->delete('Name.key); + * + * You must use this method before any output is sent to the browser. + * Failure to do so will result in header already sent errors. + * + * This method will delete both the top level and 2nd level cookies set. + * For example assuming that $name = App, deleting `User` will delete + * both `App[User]` and any other cookie values like `App[User][email]` + * This is done to clean up cookie storage from before 2.4.3, where cookies + * were stored inconsistently. + * + * @param string $key Key of the value to be deleted + * @return void + * @link https://book.cakephp.org/2.0/en/core-libraries/components/cookie.html#CookieComponent::delete + */ + public function delete($key) + { + if (empty($this->_values[$this->name])) { + $this->read(); + } + if (strpos($key, '.') === false) { + unset($this->_values[$this->name][$key]); + $this->_delete('[' . $key . ']'); + } else { + $this->_values[$this->name] = Hash::remove((array)$this->_values[$this->name], $key); + list($key) = explode('.', $key, 2); + if (isset($this->_values[$this->name][$key])) { + $value = $this->_values[$this->name][$key]; + $this->_write('[' . $key . ']', $value); + } + } + } + + /** + * Sets a cookie expire time to remove cookie value + * + * @param string $name Name of cookie + * @return void + */ + protected function _delete($name) + { + $this->_response->cookie([ + 'name' => $this->name . $name, + 'value' => '', + 'expire' => time() - 42000, + 'path' => $this->path, + 'domain' => $this->domain, + 'secure' => $this->secure, + 'httpOnly' => $this->httpOnly + ]); + } + + /** + * Will allow overriding default encryption method. Use this method + * in ex: AppController::beforeFilter() before you have read or + * written any cookies. + * + * @param string $type Encryption method + * @return void + */ + public function type($type = 'cipher') + { + $availableTypes = [ + 'cipher', + 'rijndael', + 'aes' + ]; + if (!in_array($type, $availableTypes)) { + trigger_error(__d('cake_dev', 'You must use cipher, rijndael or aes for cookie encryption type'), E_USER_WARNING); + $type = 'cipher'; + } + $this->_type = $type; + } } diff --git a/lib/Cake/Controller/Component/EmailComponent.php b/lib/Cake/Controller/Component/EmailComponent.php index 4fd96dfb..e2fe56a2 100755 --- a/lib/Cake/Controller/Component/EmailComponent.php +++ b/lib/Cake/Controller/Component/EmailComponent.php @@ -31,435 +31,444 @@ * @link https://book.cakephp.org/2.0/en/core-utility-libraries/email.html * @deprecated 3.0.0 Will be removed in 3.0. Use Network/CakeEmail instead */ -class EmailComponent extends Component { - -/** - * Recipient of the email - * - * @var string - */ - public $to = null; - -/** - * The mail which the email is sent from - * - * @var string - */ - public $from = null; - -/** - * The email the recipient will reply to - * - * @var string - */ - public $replyTo = null; - -/** - * The read receipt email - * - * @var string - */ - public $readReceipt = null; - -/** - * The mail that will be used in case of any errors like - * - Remote mailserver down - * - Remote user has exceeded his quota - * - Unknown user - * - * @var string - */ - public $return = null; - -/** - * Carbon Copy - * - * List of email's that should receive a copy of the email. - * The Recipient WILL be able to see this list - * - * @var array - */ - public $cc = array(); - -/** - * Blind Carbon Copy - * - * List of email's that should receive a copy of the email. - * The Recipient WILL NOT be able to see this list - * - * @var array - */ - public $bcc = array(); - -/** - * The date to put in the Date: header. This should be a date - * conforming with the RFC2822 standard. Leave null, to have - * today's date generated. - * - * @var string - */ - public $date = null; - -/** - * The subject of the email - * - * @var string - */ - public $subject = null; - -/** - * Associative array of a user defined headers - * Keys will be prefixed 'X-' as per RFC2822 Section 4.7.5 - * - * @var array - */ - public $headers = array(); - -/** - * List of additional headers - * - * These will NOT be used if you are using safemode and mail() - * - * @var string - */ - public $additionalParams = null; - -/** - * Layout for the View - * - * @var string - */ - public $layout = 'default'; - -/** - * Template for the view - * - * @var string - */ - public $template = null; - -/** - * Line feed character(s) to be used when sending using mail() function - * By default PHP_EOL is used. - * RFC2822 requires it to be CRLF but some Unix - * mail transfer agents replace LF by CRLF automatically - * (which leads to doubling CR if CRLF is used). - * - * @var string - */ - public $lineFeed = PHP_EOL; - -/** - * What format should the email be sent in - * - * Supported formats: - * - text - * - html - * - both - * - * @var string - */ - public $sendAs = 'text'; - -/** - * What method should the email be sent by - * - * Supported methods: - * - mail - * - smtp - * - debug - * - * @var string - */ - public $delivery = 'mail'; - -/** - * charset the email is sent in - * - * @var string - */ - public $charset = 'utf-8'; - -/** - * List of files that should be attached to the email. - * - * Can be both absolute and relative paths - * - * @var array - */ - public $attachments = array(); - -/** - * What mailer should EmailComponent identify itself as - * - * @var string - */ - public $xMailer = 'CakePHP Email Component'; - -/** - * The list of paths to search if an attachment isn't absolute - * - * @var array - */ - public $filePaths = array(); - -/** - * List of options to use for smtp mail method - * - * Options is: - * - port - * - host - * - timeout - * - username - * - password - * - client - * - * @var array - */ - public $smtpOptions = array(); - -/** - * Contains the rendered plain text message if one was sent. - * - * @var string - */ - public $textMessage = null; - -/** - * Contains the rendered HTML message if one was sent. - * - * @var string - */ - public $htmlMessage = null; - -/** - * Whether to generate a Message-ID header for the - * e-mail. True to generate a Message-ID, False to let - * it be handled by sendmail (or similar) or a string - * to completely override the Message-ID. - * - * If you are sending Email from a shell, be sure to set this value. As you - * could encounter delivery issues if you do not. - * - * @var mixed - */ - public $messageId = true; - -/** - * Controller reference - * - * @var Controller - */ - protected $_controller = null; - -/** - * Constructor - * - * @param ComponentCollection $collection A ComponentCollection this component can use to lazy load its components - * @param array $settings Array of configuration settings. - */ - public function __construct(ComponentCollection $collection, $settings = array()) { - $this->_controller = $collection->getController(); - parent::__construct($collection, $settings); - } - -/** - * Initialize component - * - * @param Controller $controller Instantiating controller - * @return void - */ - public function initialize(Controller $controller) { - if (Configure::read('App.encoding') !== null) { - $this->charset = Configure::read('App.encoding'); - } - } - -/** - * Send an email using the specified content, template and layout - * - * @param string|array $content Either an array of text lines, or a string with contents - * If you are rendering a template this variable will be sent to the templates as `$content` - * @param string $template Template to use when sending email - * @param string $layout Layout to use to enclose email body - * @return array Success - */ - public function send($content = null, $template = null, $layout = null) { - $lib = new CakeEmail(); - $lib->charset = $this->charset; - $lib->headerCharset = $this->charset; - - $lib->from($this->_formatAddresses((array)$this->from)); - if (!empty($this->to)) { - $lib->to($this->_formatAddresses((array)$this->to)); - } - if (!empty($this->cc)) { - $lib->cc($this->_formatAddresses((array)$this->cc)); - } - if (!empty($this->bcc)) { - $lib->bcc($this->_formatAddresses((array)$this->bcc)); - } - if (!empty($this->replyTo)) { - $lib->replyTo($this->_formatAddresses((array)$this->replyTo)); - } - if (!empty($this->return)) { - $lib->returnPath($this->_formatAddresses((array)$this->return)); - } - if (!empty($this->readReceipt)) { - $lib->readReceipt($this->_formatAddresses((array)$this->readReceipt)); - } - - $lib->subject($this->subject); - $lib->messageID($this->messageId); - $lib->helpers($this->_controller->helpers); - - $headers = array('X-Mailer' => $this->xMailer); - foreach ($this->headers as $key => $value) { - $headers['X-' . $key] = $value; - } - if ($this->date) { - $headers['Date'] = $this->date; - } - $lib->setHeaders($headers); - - if ($template) { - $this->template = $template; - } - if ($layout) { - $this->layout = $layout; - } - $lib->template($this->template, $this->layout)->viewVars($this->_controller->viewVars)->emailFormat($this->sendAs); - - if (!empty($this->attachments)) { - $lib->attachments($this->_formatAttachFiles()); - } - - $lib->transport(ucfirst($this->delivery)); - if ($this->delivery === 'mail') { - $lib->config(array('eol' => $this->lineFeed, 'additionalParameters' => $this->additionalParams)); - } elseif ($this->delivery === 'smtp') { - $lib->config($this->smtpOptions); - } else { - $lib->config(array()); - } - - $sent = $lib->send($content); - - $this->htmlMessage = $lib->message(CakeEmail::MESSAGE_HTML); - if (empty($this->htmlMessage)) { - $this->htmlMessage = null; - } - $this->textMessage = $lib->message(CakeEmail::MESSAGE_TEXT); - if (empty($this->textMessage)) { - $this->textMessage = null; - } - - $this->_header = array(); - $this->_message = array(); - - return $sent; - } - -/** - * Reset all EmailComponent internal variables to be able to send out a new email. - * - * @return void - */ - public function reset() { - $this->template = null; - $this->to = array(); - $this->from = null; - $this->replyTo = null; - $this->return = null; - $this->cc = array(); - $this->bcc = array(); - $this->subject = null; - $this->additionalParams = null; - $this->date = null; - $this->attachments = array(); - $this->htmlMessage = null; - $this->textMessage = null; - $this->messageId = true; - $this->delivery = 'mail'; - } - -/** - * Format the attach array - * - * @return array - */ - protected function _formatAttachFiles() { - $files = array(); - foreach ($this->attachments as $filename => $attachment) { - $file = $this->_findFiles($attachment); - if (!empty($file)) { - if (is_int($filename)) { - $filename = basename($file); - } - $files[$filename] = $file; - } - } - return $files; - } - -/** - * Find the specified attachment in the list of file paths - * - * @param string $attachment Attachment file name to find - * @return string|null Path to located file - */ - protected function _findFiles($attachment) { - if (file_exists($attachment)) { - return $attachment; - } - foreach ($this->filePaths as $path) { - if (file_exists($path . DS . $attachment)) { - $file = $path . DS . $attachment; - return $file; - } - } - return null; - } - -/** - * Format addresses to be an array with email as key and alias as value - * - * @param array $addresses Address to format. - * @return array - */ - protected function _formatAddresses($addresses) { - $formatted = array(); - foreach ($addresses as $address) { - if (preg_match('/((.*))?\s?<(.+)>/', $address, $matches) && !empty($matches[2])) { - $formatted[$this->_strip($matches[3])] = $matches[2]; - } else { - $address = $this->_strip($address); - $formatted[$address] = $address; - } - } - return $formatted; - } - -/** - * Remove certain elements (such as bcc:, to:, %0a) from given value. - * Helps prevent header injection / manipulation on user content. - * - * @param string $value Value to strip - * @param bool $message Set to true to indicate main message content - * @return string Stripped value - */ - protected function _strip($value, $message = false) { - $search = '%0a|%0d|Content-(?:Type|Transfer-Encoding)\:'; - $search .= '|charset\=|mime-version\:|multipart/mixed|(?:[^a-z]to|b?cc)\:.*'; - - if ($message !== true) { - $search .= '|\r|\n'; - } - $search = '#(?:' . $search . ')#i'; - while (preg_match($search, $value)) { - $value = preg_replace($search, '', $value); - } - return $value; - } +class EmailComponent extends Component +{ + + /** + * Recipient of the email + * + * @var string + */ + public $to = null; + + /** + * The mail which the email is sent from + * + * @var string + */ + public $from = null; + + /** + * The email the recipient will reply to + * + * @var string + */ + public $replyTo = null; + + /** + * The read receipt email + * + * @var string + */ + public $readReceipt = null; + + /** + * The mail that will be used in case of any errors like + * - Remote mailserver down + * - Remote user has exceeded his quota + * - Unknown user + * + * @var string + */ + public $return = null; + + /** + * Carbon Copy + * + * List of email's that should receive a copy of the email. + * The Recipient WILL be able to see this list + * + * @var array + */ + public $cc = []; + + /** + * Blind Carbon Copy + * + * List of email's that should receive a copy of the email. + * The Recipient WILL NOT be able to see this list + * + * @var array + */ + public $bcc = []; + + /** + * The date to put in the Date: header. This should be a date + * conforming with the RFC2822 standard. Leave null, to have + * today's date generated. + * + * @var string + */ + public $date = null; + + /** + * The subject of the email + * + * @var string + */ + public $subject = null; + + /** + * Associative array of a user defined headers + * Keys will be prefixed 'X-' as per RFC2822 Section 4.7.5 + * + * @var array + */ + public $headers = []; + + /** + * List of additional headers + * + * These will NOT be used if you are using safemode and mail() + * + * @var string + */ + public $additionalParams = null; + + /** + * Layout for the View + * + * @var string + */ + public $layout = 'default'; + + /** + * Template for the view + * + * @var string + */ + public $template = null; + + /** + * Line feed character(s) to be used when sending using mail() function + * By default PHP_EOL is used. + * RFC2822 requires it to be CRLF but some Unix + * mail transfer agents replace LF by CRLF automatically + * (which leads to doubling CR if CRLF is used). + * + * @var string + */ + public $lineFeed = PHP_EOL; + + /** + * What format should the email be sent in + * + * Supported formats: + * - text + * - html + * - both + * + * @var string + */ + public $sendAs = 'text'; + + /** + * What method should the email be sent by + * + * Supported methods: + * - mail + * - smtp + * - debug + * + * @var string + */ + public $delivery = 'mail'; + + /** + * charset the email is sent in + * + * @var string + */ + public $charset = 'utf-8'; + + /** + * List of files that should be attached to the email. + * + * Can be both absolute and relative paths + * + * @var array + */ + public $attachments = []; + + /** + * What mailer should EmailComponent identify itself as + * + * @var string + */ + public $xMailer = 'CakePHP Email Component'; + + /** + * The list of paths to search if an attachment isn't absolute + * + * @var array + */ + public $filePaths = []; + + /** + * List of options to use for smtp mail method + * + * Options is: + * - port + * - host + * - timeout + * - username + * - password + * - client + * + * @var array + */ + public $smtpOptions = []; + + /** + * Contains the rendered plain text message if one was sent. + * + * @var string + */ + public $textMessage = null; + + /** + * Contains the rendered HTML message if one was sent. + * + * @var string + */ + public $htmlMessage = null; + + /** + * Whether to generate a Message-ID header for the + * e-mail. True to generate a Message-ID, False to let + * it be handled by sendmail (or similar) or a string + * to completely override the Message-ID. + * + * If you are sending Email from a shell, be sure to set this value. As you + * could encounter delivery issues if you do not. + * + * @var mixed + */ + public $messageId = true; + + /** + * Controller reference + * + * @var Controller + */ + protected $_controller = null; + + /** + * Constructor + * + * @param ComponentCollection $collection A ComponentCollection this component can use to lazy load its components + * @param array $settings Array of configuration settings. + */ + public function __construct(ComponentCollection $collection, $settings = []) + { + $this->_controller = $collection->getController(); + parent::__construct($collection, $settings); + } + + /** + * Initialize component + * + * @param Controller $controller Instantiating controller + * @return void + */ + public function initialize(Controller $controller) + { + if (Configure::read('App.encoding') !== null) { + $this->charset = Configure::read('App.encoding'); + } + } + + /** + * Send an email using the specified content, template and layout + * + * @param string|array $content Either an array of text lines, or a string with contents + * If you are rendering a template this variable will be sent to the templates as `$content` + * @param string $template Template to use when sending email + * @param string $layout Layout to use to enclose email body + * @return array Success + */ + public function send($content = null, $template = null, $layout = null) + { + $lib = new CakeEmail(); + $lib->charset = $this->charset; + $lib->headerCharset = $this->charset; + + $lib->from($this->_formatAddresses((array)$this->from)); + if (!empty($this->to)) { + $lib->to($this->_formatAddresses((array)$this->to)); + } + if (!empty($this->cc)) { + $lib->cc($this->_formatAddresses((array)$this->cc)); + } + if (!empty($this->bcc)) { + $lib->bcc($this->_formatAddresses((array)$this->bcc)); + } + if (!empty($this->replyTo)) { + $lib->replyTo($this->_formatAddresses((array)$this->replyTo)); + } + if (!empty($this->return)) { + $lib->returnPath($this->_formatAddresses((array)$this->return)); + } + if (!empty($this->readReceipt)) { + $lib->readReceipt($this->_formatAddresses((array)$this->readReceipt)); + } + + $lib->subject($this->subject); + $lib->messageID($this->messageId); + $lib->helpers($this->_controller->helpers); + + $headers = ['X-Mailer' => $this->xMailer]; + foreach ($this->headers as $key => $value) { + $headers['X-' . $key] = $value; + } + if ($this->date) { + $headers['Date'] = $this->date; + } + $lib->setHeaders($headers); + + if ($template) { + $this->template = $template; + } + if ($layout) { + $this->layout = $layout; + } + $lib->template($this->template, $this->layout)->viewVars($this->_controller->viewVars)->emailFormat($this->sendAs); + + if (!empty($this->attachments)) { + $lib->attachments($this->_formatAttachFiles()); + } + + $lib->transport(ucfirst($this->delivery)); + if ($this->delivery === 'mail') { + $lib->config(['eol' => $this->lineFeed, 'additionalParameters' => $this->additionalParams]); + } else if ($this->delivery === 'smtp') { + $lib->config($this->smtpOptions); + } else { + $lib->config([]); + } + + $sent = $lib->send($content); + + $this->htmlMessage = $lib->message(CakeEmail::MESSAGE_HTML); + if (empty($this->htmlMessage)) { + $this->htmlMessage = null; + } + $this->textMessage = $lib->message(CakeEmail::MESSAGE_TEXT); + if (empty($this->textMessage)) { + $this->textMessage = null; + } + + $this->_header = []; + $this->_message = []; + + return $sent; + } + + /** + * Format addresses to be an array with email as key and alias as value + * + * @param array $addresses Address to format. + * @return array + */ + protected function _formatAddresses($addresses) + { + $formatted = []; + foreach ($addresses as $address) { + if (preg_match('/((.*))?\s?<(.+)>/', $address, $matches) && !empty($matches[2])) { + $formatted[$this->_strip($matches[3])] = $matches[2]; + } else { + $address = $this->_strip($address); + $formatted[$address] = $address; + } + } + return $formatted; + } + + /** + * Remove certain elements (such as bcc:, to:, %0a) from given value. + * Helps prevent header injection / manipulation on user content. + * + * @param string $value Value to strip + * @param bool $message Set to true to indicate main message content + * @return string Stripped value + */ + protected function _strip($value, $message = false) + { + $search = '%0a|%0d|Content-(?:Type|Transfer-Encoding)\:'; + $search .= '|charset\=|mime-version\:|multipart/mixed|(?:[^a-z]to|b?cc)\:.*'; + + if ($message !== true) { + $search .= '|\r|\n'; + } + $search = '#(?:' . $search . ')#i'; + while (preg_match($search, $value)) { + $value = preg_replace($search, '', $value); + } + return $value; + } + + /** + * Format the attach array + * + * @return array + */ + protected function _formatAttachFiles() + { + $files = []; + foreach ($this->attachments as $filename => $attachment) { + $file = $this->_findFiles($attachment); + if (!empty($file)) { + if (is_int($filename)) { + $filename = basename($file); + } + $files[$filename] = $file; + } + } + return $files; + } + + /** + * Find the specified attachment in the list of file paths + * + * @param string $attachment Attachment file name to find + * @return string|null Path to located file + */ + protected function _findFiles($attachment) + { + if (file_exists($attachment)) { + return $attachment; + } + foreach ($this->filePaths as $path) { + if (file_exists($path . DS . $attachment)) { + $file = $path . DS . $attachment; + return $file; + } + } + return null; + } + + /** + * Reset all EmailComponent internal variables to be able to send out a new email. + * + * @return void + */ + public function reset() + { + $this->template = null; + $this->to = []; + $this->from = null; + $this->replyTo = null; + $this->return = null; + $this->cc = []; + $this->bcc = []; + $this->subject = null; + $this->additionalParams = null; + $this->date = null; + $this->attachments = []; + $this->htmlMessage = null; + $this->textMessage = null; + $this->messageId = true; + $this->delivery = 'mail'; + } } diff --git a/lib/Cake/Controller/Component/FlashComponent.php b/lib/Cake/Controller/Component/FlashComponent.php index 51e40ebf..c6f03146 100644 --- a/lib/Cake/Controller/Component/FlashComponent.php +++ b/lib/Cake/Controller/Component/FlashComponent.php @@ -27,102 +27,106 @@ * * @package Cake.Controller.Component */ -class FlashComponent extends Component { - -/** - * Default configuration - * - * @var array - */ - protected $_defaultConfig = array( - 'key' => 'flash', - 'element' => 'default', - 'params' => array(), - 'clear' => false - ); - -/** - * Constructor - * - * @param ComponentCollection $collection The ComponentCollection object - * @param array $settings Settings passed via controller - */ - public function __construct(ComponentCollection $collection, $settings = array()) { - $this->_defaultConfig = Hash::merge($this->_defaultConfig, $settings); - } - -/** - * Used to set a session variable that can be used to output messages in the view. - * - * In your controller: $this->Flash->set('This has been saved'); - * - * ### Options: - * - * - `key` The key to set under the session's Flash key - * - `element` The element used to render the flash message. Default to 'default'. - * - `params` An array of variables to make available when using an element - * - * @param string $message Message to be flashed. If an instance - * of Exception the exception message will be used and code will be set - * in params. - * @param array $options An array of options. - * @return void - */ - - public function set($message, $options = array()) { - $options += $this->_defaultConfig; - - if ($message instanceof Exception) { - $options['params'] += array('code' => $message->getCode()); - $message = $message->getMessage(); - } - - list($plugin, $element) = pluginSplit($options['element'], true); - if (!empty($options['plugin'])) { - $plugin = $options['plugin'] . '.'; - } - $options['element'] = $plugin . 'Flash/' . $element; - - $messages = array(); - if ($options['clear'] === false) { - $messages = (array)CakeSession::read('Message.' . $options['key']); - } - - $newMessage = array( - 'message' => $message, - 'key' => $options['key'], - 'element' => $options['element'], - 'params' => $options['params'] - ); - - $messages[] = $newMessage; - - CakeSession::write('Message.' . $options['key'], $messages); - } - -/** - * Magic method for verbose flash methods based on element names. - * - * For example: $this->Flash->success('My message') would use the - * success.ctp element under `app/View/Element/Flash` for rendering the - * flash message. - * - * @param string $name Element name to use. - * @param array $args Parameters to pass when calling `FlashComponent::set()`. - * @return void - * @throws InternalErrorException If missing the flash message. - */ - public function __call($name, $args) { - $options = array('element' => Inflector::underscore($name)); - - if (count($args) < 1) { - throw new InternalErrorException('Flash message missing.'); - } - - if (!empty($args[1])) { - $options += (array)$args[1]; - } - - $this->set($args[0], $options); - } +class FlashComponent extends Component +{ + + /** + * Default configuration + * + * @var array + */ + protected $_defaultConfig = [ + 'key' => 'flash', + 'element' => 'default', + 'params' => [], + 'clear' => false + ]; + + /** + * Constructor + * + * @param ComponentCollection $collection The ComponentCollection object + * @param array $settings Settings passed via controller + */ + public function __construct(ComponentCollection $collection, $settings = []) + { + $this->_defaultConfig = Hash::merge($this->_defaultConfig, $settings); + } + + /** + * Magic method for verbose flash methods based on element names. + * + * For example: $this->Flash->success('My message') would use the + * success.ctp element under `app/View/Element/Flash` for rendering the + * flash message. + * + * @param string $name Element name to use. + * @param array $args Parameters to pass when calling `FlashComponent::set()`. + * @return void + * @throws InternalErrorException If missing the flash message. + */ + public function __call($name, $args) + { + $options = ['element' => Inflector::underscore($name)]; + + if (count($args) < 1) { + throw new InternalErrorException('Flash message missing.'); + } + + if (!empty($args[1])) { + $options += (array)$args[1]; + } + + $this->set($args[0], $options); + } + + /** + * Used to set a session variable that can be used to output messages in the view. + * + * In your controller: $this->Flash->set('This has been saved'); + * + * ### Options: + * + * - `key` The key to set under the session's Flash key + * - `element` The element used to render the flash message. Default to 'default'. + * - `params` An array of variables to make available when using an element + * + * @param string $message Message to be flashed. If an instance + * of Exception the exception message will be used and code will be set + * in params. + * @param array $options An array of options. + * @return void + */ + + public function set($message, $options = []) + { + $options += $this->_defaultConfig; + + if ($message instanceof Exception) { + $options['params'] += ['code' => $message->getCode()]; + $message = $message->getMessage(); + } + + list($plugin, $element) = pluginSplit($options['element'], true); + if (!empty($options['plugin'])) { + $plugin = $options['plugin'] . '.'; + } + $options['element'] = $plugin . 'Flash/' . $element; + + $messages = []; + if ($options['clear'] === false) { + $messages = (array)CakeSession::read('Message.' . $options['key']); + } + + $newMessage = [ + 'message' => $message, + 'key' => $options['key'], + 'element' => $options['element'], + 'params' => $options['params'] + ]; + + $messages[] = $newMessage; + + CakeSession::write('Message.' . $options['key'], $messages); + } } diff --git a/lib/Cake/Controller/Component/PaginatorComponent.php b/lib/Cake/Controller/Component/PaginatorComponent.php index da541708..55d80439 100755 --- a/lib/Cake/Controller/Component/PaginatorComponent.php +++ b/lib/Cake/Controller/Component/PaginatorComponent.php @@ -30,23 +30,23 @@ * are no specific model configuration, or the model you are paginating does not have specific settings. * * ``` - * $this->Paginator->settings = array( - * 'limit' => 20, - * 'maxLimit' => 100 - * ); + * $this->Paginator->settings = array( + * 'limit' => 20, + * 'maxLimit' => 100 + * ); * ``` * * The above settings will be used to paginate any model. You can configure model specific settings by * keying the settings with the model name. * * ``` - * $this->Paginator->settings = array( - * 'Post' => array( - * 'limit' => 20, - * 'maxLimit' => 100 - * ), - * 'Comment' => array( ... ) - * ); + * $this->Paginator->settings = array( + * 'Post' => array( + * 'limit' => 20, + * 'maxLimit' => 100 + * ), + * 'Comment' => array( ... ) + * ); * ``` * * This would allow you to have different pagination settings for `Comment` and `Post` models. @@ -57,9 +57,9 @@ * * ``` * $this->Paginator->settings = array( - * 'Post' => array( - * 'findType' => 'popular' - * ) + * 'Post' => array( + * 'findType' => 'popular' + * ) * ); * ``` * @@ -68,383 +68,391 @@ * @package Cake.Controller.Component * @link https://book.cakephp.org/2.0/en/core-libraries/components/pagination.html */ -class PaginatorComponent extends Component { - -/** - * Pagination settings. These settings control pagination at a general level. - * You can also define sub arrays for pagination settings for specific models. - * - * - `maxLimit` The maximum limit users can choose to view. Defaults to 100 - * - `limit` The initial number of items per page. Defaults to 20. - * - `page` The starting page, defaults to 1. - * - `paramType` What type of parameters you want pagination to use? - * - `named` Use named parameters / routed parameters. - * - `querystring` Use query string parameters. - * - `queryScope` By using request parameter scopes you can paginate multiple queries in the same controller action. - * - * ``` - * $paginator->paginate = array( - * 'Article' => array('queryScope' => 'articles'), - * 'Tag' => array('queryScope' => 'tags'), - * ); - * ``` - * - * Each of the above queries will use different query string parameter sets - * for pagination data. An example URL paginating both results would be: - * - * ``` - * /dashboard/articles[page]:1/tags[page]:2 - * ``` - * - * @var array - */ - public $settings = array( - 'page' => 1, - 'limit' => 20, - 'maxLimit' => 100, - 'paramType' => 'named', - 'queryScope' => null - ); - -/** - * A list of parameters users are allowed to set using request parameters. Modifying - * this list will allow users to have more influence over pagination, - * be careful with what you permit. - * - * @var array - */ - public $whitelist = array( - 'limit', 'sort', 'page', 'direction' - ); - -/** - * Constructor - * - * @param ComponentCollection $collection A ComponentCollection this component can use to lazy load its components - * @param array $settings Array of configuration settings. - */ - public function __construct(ComponentCollection $collection, $settings = array()) { - $settings = array_merge($this->settings, (array)$settings); - $this->Controller = $collection->getController(); - parent::__construct($collection, $settings); - } - -/** - * Handles automatic pagination of model records. - * - * @param Model|string $object Model to paginate (e.g: model instance, or 'Model', or 'Model.InnerModel') - * @param string|array $scope Additional find conditions to use while paginating - * @param array $whitelist List of allowed fields for ordering. This allows you to prevent ordering - * on non-indexed, or undesirable columns. See PaginatorComponent::validateSort() for additional details - * on how the whitelisting and sort field validation works. - * @return array Model query results - * @throws MissingModelException - * @throws NotFoundException - */ - public function paginate($object = null, $scope = array(), $whitelist = array()) { - if (is_array($object)) { - $whitelist = $scope; - $scope = $object; - $object = null; - } - - $object = $this->_getObject($object); - - if (!is_object($object)) { - throw new MissingModelException($object); - } - - $options = $this->mergeOptions($object->alias); - $options = $this->validateSort($object, $options, $whitelist); - $options = $this->checkLimit($options); - - $conditions = $fields = $order = $limit = $page = $recursive = null; - - if (!isset($options['conditions'])) { - $options['conditions'] = array(); - } - - $type = 'all'; - - if (isset($options[0])) { - $type = $options[0]; - unset($options[0]); - } - - extract($options); - - if (is_array($scope) && !empty($scope)) { - $conditions = array_merge($conditions, $scope); - } elseif (is_string($scope)) { - $conditions = array($conditions, $scope); - } - if ($recursive === null) { - $recursive = $object->recursive; - } - - $extra = array_diff_key($options, compact( - 'conditions', 'fields', 'order', 'limit', 'page', 'recursive' - )); - - if (!empty($extra['findType'])) { - $type = $extra['findType']; - unset($extra['findType']); - } - - if ($type !== 'all') { - $extra['type'] = $type; - } - - if ((int)$page < 1) { - $page = 1; - } - $page = $options['page'] = (int)$page; - - if ($object->hasMethod('paginate')) { - $results = $object->paginate( - $conditions, $fields, $order, $limit, $page, $recursive, $extra - ); - } else { - $parameters = compact('conditions', 'fields', 'order', 'limit', 'page'); - if ($recursive != $object->recursive) { - $parameters['recursive'] = $recursive; - } - $results = $object->find($type, array_merge($parameters, $extra)); - } - $defaults = $this->getDefaults($object->alias); - unset($defaults[0]); - - if (!$results) { - $count = 0; - } elseif ($object->hasMethod('paginateCount')) { - $count = $object->paginateCount($conditions, $recursive, $extra); - } elseif ($page === 1 && count($results) < $limit) { - $count = count($results); - } else { - $parameters = compact('conditions'); - if ($recursive != $object->recursive) { - $parameters['recursive'] = $recursive; - } - $count = $object->find('count', array_merge($parameters, $extra)); - } - $pageCount = (int)ceil($count / $limit); - $requestedPage = $page; - $page = max(min($page, $pageCount), 1); - - $paging = array( - 'page' => $page, - 'current' => count($results), - 'count' => $count, - 'prevPage' => ($page > 1), - 'nextPage' => ($count > ($page * $limit)), - 'pageCount' => $pageCount, - 'order' => $order, - 'limit' => $limit, - 'options' => Hash::diff($options, $defaults), - 'paramType' => $options['paramType'], - 'queryScope' => $options['queryScope'], - ); - - if (!isset($this->Controller->request['paging'])) { - $this->Controller->request['paging'] = array(); - } - $this->Controller->request['paging'] = array_merge( - (array)$this->Controller->request['paging'], - array($object->alias => $paging) - ); - - if ($requestedPage > $page) { - throw new NotFoundException(); - } - - if (!in_array('Paginator', $this->Controller->helpers) && - !array_key_exists('Paginator', $this->Controller->helpers) - ) { - $this->Controller->helpers[] = 'Paginator'; - } - return $results; - } - -/** - * Get the object pagination will occur on. - * - * @param string|Model $object The object you are looking for. - * @return mixed The model object to paginate on. - */ - protected function _getObject($object) { - if (is_string($object)) { - $assoc = null; - if (strpos($object, '.') !== false) { - list($object, $assoc) = pluginSplit($object); - } - if ($assoc && isset($this->Controller->{$object}->{$assoc})) { - return $this->Controller->{$object}->{$assoc}; - } - if ($assoc && isset($this->Controller->{$this->Controller->modelClass}->{$assoc})) { - return $this->Controller->{$this->Controller->modelClass}->{$assoc}; - } - if (isset($this->Controller->{$object})) { - return $this->Controller->{$object}; - } - if (isset($this->Controller->{$this->Controller->modelClass}->{$object})) { - return $this->Controller->{$this->Controller->modelClass}->{$object}; - } - } - if (empty($object) || $object === null) { - if (isset($this->Controller->{$this->Controller->modelClass})) { - return $this->Controller->{$this->Controller->modelClass}; - } - - $className = null; - $name = $this->Controller->uses[0]; - if (strpos($this->Controller->uses[0], '.') !== false) { - list($name, $className) = explode('.', $this->Controller->uses[0]); - } - if ($className) { - return $this->Controller->{$className}; - } - - return $this->Controller->{$name}; - } - return $object; - } - -/** - * Merges the various options that Pagination uses. - * Pulls settings together from the following places: - * - * - General pagination settings - * - Model specific settings. - * - Request parameters - * - * The result of this method is the aggregate of all the option sets combined together. You can change - * PaginatorComponent::$whitelist to modify which options/values can be set using request parameters. - * - * @param string $alias Model alias being paginated, if the general settings has a key with this value - * that key's settings will be used for pagination instead of the general ones. - * @return array Array of merged options. - */ - public function mergeOptions($alias) { - $defaults = $this->getDefaults($alias); - switch ($defaults['paramType']) { - case 'named': - $request = $this->Controller->request->params['named']; - break; - case 'querystring': - $request = $this->Controller->request->query; - break; - } - if ($defaults['queryScope']) { - $request = Hash::get($request, $defaults['queryScope'], array()); - } - $request = array_intersect_key($request, array_flip($this->whitelist)); - return array_merge($defaults, $request); - } - -/** - * Get the default settings for a $model. If there are no settings for a specific model, the general settings - * will be used. - * - * @param string $alias Model name to get default settings for. - * @return array An array of pagination defaults for a model, or the general settings. - */ - public function getDefaults($alias) { - $defaults = $this->settings; - if (isset($this->settings[$alias])) { - $defaults = $this->settings[$alias]; - } - $defaults += array( - 'page' => 1, - 'limit' => 20, - 'maxLimit' => 100, - 'paramType' => 'named', - 'queryScope' => null - ); - return $defaults; - } - -/** - * Validate that the desired sorting can be performed on the $object. Only fields or - * virtualFields can be sorted on. The direction param will also be sanitized. Lastly - * sort + direction keys will be converted into the model friendly order key. - * - * You can use the whitelist parameter to control which columns/fields are available for sorting. - * This helps prevent users from ordering large result sets on un-indexed values. - * - * Any columns listed in the sort whitelist will be implicitly trusted. You can use this to sort - * on synthetic columns, or columns added in custom find operations that may not exist in the schema. - * - * @param Model $object The model being paginated. - * @param array $options The pagination options being used for this request. - * @param array $whitelist The list of columns that can be used for sorting. If empty all keys are allowed. - * @return array An array of options with sort + direction removed and replaced with order if possible. - */ - public function validateSort(Model $object, array $options, array $whitelist = array()) { - if (empty($options['order']) && is_array($object->order)) { - $options['order'] = $object->order; - } - - if (isset($options['sort'])) { - $direction = null; - if (isset($options['direction'])) { - $direction = strtolower($options['direction']); - } - if (!in_array($direction, array('asc', 'desc'))) { - $direction = 'asc'; - } - $options['order'] = array($options['sort'] => $direction); - } - - if (!empty($whitelist) && isset($options['order']) && is_array($options['order'])) { - $field = key($options['order']); - $inWhitelist = in_array($field, $whitelist, true); - if (!$inWhitelist) { - $options['order'] = null; - } - return $options; - } - if (!empty($options['order']) && is_array($options['order'])) { - $order = array(); - foreach ($options['order'] as $key => $value) { - if (is_int($key)) { - $field = explode(' ', $value); - $key = $field[0]; - $value = count($field) === 2 ? trim($field[1]) : 'asc'; - } - $field = $key; - $alias = $object->alias; - if (strpos($key, '.') !== false) { - list($alias, $field) = explode('.', $key); - } - $correctAlias = ($object->alias === $alias); - - if ($correctAlias && $object->hasField($field)) { - $order[$object->alias . '.' . $field] = $value; - } elseif ($correctAlias && $object->hasField($key, true)) { - $order[$field] = $value; - } elseif (isset($object->{$alias}) && $object->{$alias}->hasField($field, true)) { - $order[$alias . '.' . $field] = $value; - } - } - $options['order'] = $order; - } - - return $options; - } - -/** - * Check the limit parameter and ensure its within the maxLimit bounds. - * - * @param array $options An array of options with a limit key to be checked. - * @return array An array of options for pagination - */ - public function checkLimit(array $options) { - $options['limit'] = (int)$options['limit']; - if (empty($options['limit']) || $options['limit'] < 1) { - $options['limit'] = 1; - } - $options['limit'] = min($options['limit'], $options['maxLimit']); - return $options; - } +class PaginatorComponent extends Component +{ + + /** + * Pagination settings. These settings control pagination at a general level. + * You can also define sub arrays for pagination settings for specific models. + * + * - `maxLimit` The maximum limit users can choose to view. Defaults to 100 + * - `limit` The initial number of items per page. Defaults to 20. + * - `page` The starting page, defaults to 1. + * - `paramType` What type of parameters you want pagination to use? + * - `named` Use named parameters / routed parameters. + * - `querystring` Use query string parameters. + * - `queryScope` By using request parameter scopes you can paginate multiple queries in the same controller action. + * + * ``` + * $paginator->paginate = array( + * 'Article' => array('queryScope' => 'articles'), + * 'Tag' => array('queryScope' => 'tags'), + * ); + * ``` + * + * Each of the above queries will use different query string parameter sets + * for pagination data. An example URL paginating both results would be: + * + * ``` + * /dashboard/articles[page]:1/tags[page]:2 + * ``` + * + * @var array + */ + public $settings = [ + 'page' => 1, + 'limit' => 20, + 'maxLimit' => 100, + 'paramType' => 'named', + 'queryScope' => null + ]; + + /** + * A list of parameters users are allowed to set using request parameters. Modifying + * this list will allow users to have more influence over pagination, + * be careful with what you permit. + * + * @var array + */ + public $whitelist = [ + 'limit', 'sort', 'page', 'direction' + ]; + + /** + * Constructor + * + * @param ComponentCollection $collection A ComponentCollection this component can use to lazy load its components + * @param array $settings Array of configuration settings. + */ + public function __construct(ComponentCollection $collection, $settings = []) + { + $settings = array_merge($this->settings, (array)$settings); + $this->Controller = $collection->getController(); + parent::__construct($collection, $settings); + } + + /** + * Handles automatic pagination of model records. + * + * @param Model|string $object Model to paginate (e.g: model instance, or 'Model', or 'Model.InnerModel') + * @param string|array $scope Additional find conditions to use while paginating + * @param array $whitelist List of allowed fields for ordering. This allows you to prevent ordering + * on non-indexed, or undesirable columns. See PaginatorComponent::validateSort() for additional details + * on how the whitelisting and sort field validation works. + * @return array Model query results + * @throws MissingModelException + * @throws NotFoundException + */ + public function paginate($object = null, $scope = [], $whitelist = []) + { + if (is_array($object)) { + $whitelist = $scope; + $scope = $object; + $object = null; + } + + $object = $this->_getObject($object); + + if (!is_object($object)) { + throw new MissingModelException($object); + } + + $options = $this->mergeOptions($object->alias); + $options = $this->validateSort($object, $options, $whitelist); + $options = $this->checkLimit($options); + + $conditions = $fields = $order = $limit = $page = $recursive = null; + + if (!isset($options['conditions'])) { + $options['conditions'] = []; + } + + $type = 'all'; + + if (isset($options[0])) { + $type = $options[0]; + unset($options[0]); + } + + extract($options); + + if (is_array($scope) && !empty($scope)) { + $conditions = array_merge($conditions, $scope); + } else if (is_string($scope)) { + $conditions = [$conditions, $scope]; + } + if ($recursive === null) { + $recursive = $object->recursive; + } + + $extra = array_diff_key($options, compact( + 'conditions', 'fields', 'order', 'limit', 'page', 'recursive' + )); + + if (!empty($extra['findType'])) { + $type = $extra['findType']; + unset($extra['findType']); + } + + if ($type !== 'all') { + $extra['type'] = $type; + } + + if ((int)$page < 1) { + $page = 1; + } + $page = $options['page'] = (int)$page; + + if ($object->hasMethod('paginate')) { + $results = $object->paginate( + $conditions, $fields, $order, $limit, $page, $recursive, $extra + ); + } else { + $parameters = compact('conditions', 'fields', 'order', 'limit', 'page'); + if ($recursive != $object->recursive) { + $parameters['recursive'] = $recursive; + } + $results = $object->find($type, array_merge($parameters, $extra)); + } + $defaults = $this->getDefaults($object->alias); + unset($defaults[0]); + + if (!$results) { + $count = 0; + } else if ($object->hasMethod('paginateCount')) { + $count = $object->paginateCount($conditions, $recursive, $extra); + } else if ($page === 1 && count($results) < $limit) { + $count = count($results); + } else { + $parameters = compact('conditions'); + if ($recursive != $object->recursive) { + $parameters['recursive'] = $recursive; + } + $count = $object->find('count', array_merge($parameters, $extra)); + } + $pageCount = (int)ceil($count / $limit); + $requestedPage = $page; + $page = max(min($page, $pageCount), 1); + + $paging = [ + 'page' => $page, + 'current' => count($results), + 'count' => $count, + 'prevPage' => ($page > 1), + 'nextPage' => ($count > ($page * $limit)), + 'pageCount' => $pageCount, + 'order' => $order, + 'limit' => $limit, + 'options' => Hash::diff($options, $defaults), + 'paramType' => $options['paramType'], + 'queryScope' => $options['queryScope'], + ]; + + if (!isset($this->Controller->request['paging'])) { + $this->Controller->request['paging'] = []; + } + $this->Controller->request['paging'] = array_merge( + (array)$this->Controller->request['paging'], + [$object->alias => $paging] + ); + + if ($requestedPage > $page) { + throw new NotFoundException(); + } + + if (!in_array('Paginator', $this->Controller->helpers) && + !array_key_exists('Paginator', $this->Controller->helpers) + ) { + $this->Controller->helpers[] = 'Paginator'; + } + return $results; + } + + /** + * Get the object pagination will occur on. + * + * @param string|Model $object The object you are looking for. + * @return mixed The model object to paginate on. + */ + protected function _getObject($object) + { + if (is_string($object)) { + $assoc = null; + if (strpos($object, '.') !== false) { + list($object, $assoc) = pluginSplit($object); + } + if ($assoc && isset($this->Controller->{$object}->{$assoc})) { + return $this->Controller->{$object}->{$assoc}; + } + if ($assoc && isset($this->Controller->{$this->Controller->modelClass}->{$assoc})) { + return $this->Controller->{$this->Controller->modelClass}->{$assoc}; + } + if (isset($this->Controller->{$object})) { + return $this->Controller->{$object}; + } + if (isset($this->Controller->{$this->Controller->modelClass}->{$object})) { + return $this->Controller->{$this->Controller->modelClass}->{$object}; + } + } + if (empty($object) || $object === null) { + if (isset($this->Controller->{$this->Controller->modelClass})) { + return $this->Controller->{$this->Controller->modelClass}; + } + + $className = null; + $name = $this->Controller->uses[0]; + if (strpos($this->Controller->uses[0], '.') !== false) { + list($name, $className) = explode('.', $this->Controller->uses[0]); + } + if ($className) { + return $this->Controller->{$className}; + } + + return $this->Controller->{$name}; + } + return $object; + } + + /** + * Merges the various options that Pagination uses. + * Pulls settings together from the following places: + * + * - General pagination settings + * - Model specific settings. + * - Request parameters + * + * The result of this method is the aggregate of all the option sets combined together. You can change + * PaginatorComponent::$whitelist to modify which options/values can be set using request parameters. + * + * @param string $alias Model alias being paginated, if the general settings has a key with this value + * that key's settings will be used for pagination instead of the general ones. + * @return array Array of merged options. + */ + public function mergeOptions($alias) + { + $defaults = $this->getDefaults($alias); + switch ($defaults['paramType']) { + case 'named': + $request = $this->Controller->request->params['named']; + break; + case 'querystring': + $request = $this->Controller->request->query; + break; + } + if ($defaults['queryScope']) { + $request = Hash::get($request, $defaults['queryScope'], []); + } + $request = array_intersect_key($request, array_flip($this->whitelist)); + return array_merge($defaults, $request); + } + + /** + * Get the default settings for a $model. If there are no settings for a specific model, the general settings + * will be used. + * + * @param string $alias Model name to get default settings for. + * @return array An array of pagination defaults for a model, or the general settings. + */ + public function getDefaults($alias) + { + $defaults = $this->settings; + if (isset($this->settings[$alias])) { + $defaults = $this->settings[$alias]; + } + $defaults += [ + 'page' => 1, + 'limit' => 20, + 'maxLimit' => 100, + 'paramType' => 'named', + 'queryScope' => null + ]; + return $defaults; + } + + /** + * Validate that the desired sorting can be performed on the $object. Only fields or + * virtualFields can be sorted on. The direction param will also be sanitized. Lastly + * sort + direction keys will be converted into the model friendly order key. + * + * You can use the whitelist parameter to control which columns/fields are available for sorting. + * This helps prevent users from ordering large result sets on un-indexed values. + * + * Any columns listed in the sort whitelist will be implicitly trusted. You can use this to sort + * on synthetic columns, or columns added in custom find operations that may not exist in the schema. + * + * @param Model $object The model being paginated. + * @param array $options The pagination options being used for this request. + * @param array $whitelist The list of columns that can be used for sorting. If empty all keys are allowed. + * @return array An array of options with sort + direction removed and replaced with order if possible. + */ + public function validateSort(Model $object, array $options, array $whitelist = []) + { + if (empty($options['order']) && is_array($object->order)) { + $options['order'] = $object->order; + } + + if (isset($options['sort'])) { + $direction = null; + if (isset($options['direction'])) { + $direction = strtolower($options['direction']); + } + if (!in_array($direction, ['asc', 'desc'])) { + $direction = 'asc'; + } + $options['order'] = [$options['sort'] => $direction]; + } + + if (!empty($whitelist) && isset($options['order']) && is_array($options['order'])) { + $field = key($options['order']); + $inWhitelist = in_array($field, $whitelist, true); + if (!$inWhitelist) { + $options['order'] = null; + } + return $options; + } + if (!empty($options['order']) && is_array($options['order'])) { + $order = []; + foreach ($options['order'] as $key => $value) { + if (is_int($key)) { + $field = explode(' ', $value); + $key = $field[0]; + $value = count($field) === 2 ? trim($field[1]) : 'asc'; + } + $field = $key; + $alias = $object->alias; + if (strpos($key, '.') !== false) { + list($alias, $field) = explode('.', $key); + } + $correctAlias = ($object->alias === $alias); + + if ($correctAlias && $object->hasField($field)) { + $order[$object->alias . '.' . $field] = $value; + } else if ($correctAlias && $object->hasField($key, true)) { + $order[$field] = $value; + } else if (isset($object->{$alias}) && $object->{$alias}->hasField($field, true)) { + $order[$alias . '.' . $field] = $value; + } + } + $options['order'] = $order; + } + + return $options; + } + + /** + * Check the limit parameter and ensure its within the maxLimit bounds. + * + * @param array $options An array of options with a limit key to be checked. + * @return array An array of options for pagination + */ + public function checkLimit(array $options) + { + $options['limit'] = (int)$options['limit']; + if (empty($options['limit']) || $options['limit'] < 1) { + $options['limit'] = 1; + } + $options['limit'] = min($options['limit'], $options['maxLimit']); + return $options; + } } diff --git a/lib/Cake/Controller/Component/RequestHandlerComponent.php b/lib/Cake/Controller/Component/RequestHandlerComponent.php index 1915b981..e5471e69 100755 --- a/lib/Cake/Controller/Component/RequestHandlerComponent.php +++ b/lib/Cake/Controller/Component/RequestHandlerComponent.php @@ -33,764 +33,798 @@ * @package Cake.Controller.Component * @link https://book.cakephp.org/2.0/en/core-libraries/components/request-handling.html */ -class RequestHandlerComponent extends Component { - -/** - * The layout that will be switched to for Ajax requests - * - * @var string - * @see RequestHandler::setAjax() - */ - public $ajaxLayout = 'ajax'; - -/** - * Determines whether or not callbacks will be fired on this component - * - * @var bool - */ - public $enabled = true; - -/** - * Holds the reference to Controller::$request - * - * @var CakeRequest - */ - public $request; - -/** - * Holds the reference to Controller::$response - * - * @var CakeResponse - */ - public $response; - -/** - * Contains the file extension parsed out by the Router - * - * @var string - * @see Router::parseExtensions() - */ - public $ext = null; - -/** - * Array of parameters parsed from the URL. - * - * @var array|null - */ - public $params = null; - -/** - * The template to use when rendering the given content type. - * - * @var string - */ - protected $_renderType = null; - -/** - * A mapping between extensions and deserializers for request bodies of that type. - * By default only JSON and XML are mapped, use RequestHandlerComponent::addInputType() - * - * @var array - */ - protected $_inputTypeMap = array( - 'json' => array('json_decode', true) - ); - -/** - * A mapping between type and viewClass - * By default only JSON and XML are mapped, use RequestHandlerComponent::viewClassMap() - * - * @var array - */ - protected $_viewClassMap = array( - 'json' => 'Json', - 'xml' => 'Xml' - ); - -/** - * Constructor. Parses the accepted content types accepted by the client using HTTP_ACCEPT - * - * @param ComponentCollection $collection ComponentCollection object. - * @param array $settings Array of settings. - */ - public function __construct(ComponentCollection $collection, $settings = array()) { - parent::__construct($collection, $settings + array('checkHttpCache' => true)); - $this->addInputType('xml', array(array($this, 'convertXml'))); - - $Controller = $collection->getController(); - $this->request = $Controller->request; - $this->response = $Controller->response; - } - -/** - * Checks to see if a file extension has been parsed by the Router, or if the - * HTTP_ACCEPT_TYPE has matches only one content type with the supported extensions. - * If there is only one matching type between the supported content types & extensions, - * and the requested mime-types, RequestHandler::$ext is set to that value. - * - * @param Controller $controller A reference to the controller - * @return void - * @see Router::parseExtensions() - */ - public function initialize(Controller $controller) { - if (isset($this->request->params['ext'])) { - $this->ext = $this->request->params['ext']; - } - if (empty($this->ext) || $this->ext === 'html') { - $this->_setExtension(); - } - $this->params = $controller->request->params; - if (!empty($this->settings['viewClassMap'])) { - $this->viewClassMap($this->settings['viewClassMap']); - } - } - -/** - * Set the extension based on the accept headers. - * Compares the accepted types and configured extensions. - * If there is one common type, that is assigned as the ext/content type - * for the response. - * Type with the highest weight will be set. If the highest weight has more - * then one type matching the extensions, the order in which extensions are specified - * determines which type will be set. - * - * If html is one of the preferred types, no content type will be set, this - * is to avoid issues with browsers that prefer html and several other content types. - * - * @return void - */ - protected function _setExtension() { - $accept = $this->request->parseAccept(); - if (empty($accept)) { - return; - } - - $accepts = $this->response->mapType($accept); - $preferedTypes = current($accepts); - if (array_intersect($preferedTypes, array('html', 'xhtml'))) { - return; - } - - $extensions = Router::extensions(); - foreach ($accepts as $types) { - $ext = array_intersect($extensions, $types); - if ($ext) { - $this->ext = current($ext); - break; - } - } - } - -/** - * The startup method of the RequestHandler enables several automatic behaviors - * related to the detection of certain properties of the HTTP request, including: - * - * - Disabling layout rendering for Ajax requests (based on the HTTP_X_REQUESTED_WITH header) - * - If Router::parseExtensions() is enabled, the layout and template type are - * switched based on the parsed extension or Accept-Type header. For example, if `controller/action.xml` - * is requested, the view path becomes `app/View/Controller/xml/action.ctp`. Also if - * `controller/action` is requested with `Accept-Type: application/xml` in the headers - * the view path will become `app/View/Controller/xml/action.ctp`. Layout and template - * types will only switch to mime-types recognized by CakeResponse. If you need to declare - * additional mime-types, you can do so using CakeResponse::type() in your controllers beforeFilter() - * method. - * - If a helper with the same name as the extension exists, it is added to the controller. - * - If the extension is of a type that RequestHandler understands, it will set that - * Content-type in the response header. - * - If the XML data is POSTed, the data is parsed into an XML object, which is assigned - * to the $data property of the controller, which can then be saved to a model object. - * - * @param Controller $controller A reference to the controller - * @return void - */ - public function startup(Controller $controller) { - $controller->request->params['isAjax'] = $this->request->is('ajax'); - $isRecognized = ( - !in_array($this->ext, array('html', 'htm')) && - $this->response->getMimeType($this->ext) - ); - - if (!empty($this->ext) && $isRecognized) { - $this->renderAs($controller, $this->ext); - } elseif ($this->request->is('ajax')) { - $this->renderAs($controller, 'ajax'); - } elseif (empty($this->ext) || in_array($this->ext, array('html', 'htm'))) { - $this->respondAs('html', array('charset' => Configure::read('App.encoding'))); - } - - foreach ($this->_inputTypeMap as $type => $handler) { - if ($this->requestedWith($type)) { - $input = (array)call_user_func_array(array($controller->request, 'input'), $handler); - $controller->request->data = $input; - } - } - } - -/** - * Helper method to parse xml input data, due to lack of anonymous functions - * this lives here. - * - * @param string $xml XML string. - * @return array Xml array data - */ - public function convertXml($xml) { - try { - $xml = Xml::build($xml, array('readFile' => false)); - if (isset($xml->data)) { - return Xml::toArray($xml->data); - } - return Xml::toArray($xml); - } catch (XmlException $e) { - return array(); - } - } - -/** - * Handles (fakes) redirects for Ajax requests using requestAction() - * Modifies the $_POST and $_SERVER['REQUEST_METHOD'] to simulate a new GET request. - * - * @param Controller $controller A reference to the controller - * @param string|array $url A string or array containing the redirect location - * @param int|array $status HTTP Status for redirect - * @param bool $exit Whether to exit script, defaults to `true`. - * @return void - */ - public function beforeRedirect(Controller $controller, $url, $status = null, $exit = true) { - if (!$this->request->is('ajax')) { - return; - } - if (empty($url)) { - return; - } - $_SERVER['REQUEST_METHOD'] = 'GET'; - foreach ($_POST as $key => $val) { - unset($_POST[$key]); - } - if (is_array($url)) { - $url = Router::url($url + array('base' => false)); - } - if (!empty($status)) { - $statusCode = $this->response->httpCodes($status); - if (is_array($statusCode)) { - $code = key($statusCode); - $this->response->statusCode($code); - } - } - $this->response->body($this->requestAction($url, array('return', 'bare' => false))); - $this->response->send(); - $this->_stop(); - } - -/** - * Checks if the response can be considered different according to the request - * headers, and the caching response headers. If it was not modified, then the - * render process is skipped. And the client will get a blank response with a - * "304 Not Modified" header. - * - * @param Controller $controller Controller instance. - * @return bool False if the render process should be aborted. - */ - public function beforeRender(Controller $controller) { - if ($this->settings['checkHttpCache'] && $this->response->checkNotModified($this->request)) { - return false; - } - } - -/** - * Returns true if the current HTTP request is Ajax, false otherwise - * - * @return bool True if call is Ajax - * @deprecated 3.0.0 Use `$this->request->is('ajax')` instead. - */ - public function isAjax() { - return $this->request->is('ajax'); - } - -/** - * Returns true if the current HTTP request is coming from a Flash-based client - * - * @return bool True if call is from Flash - * @deprecated 3.0.0 Use `$this->request->is('flash')` instead. - */ - public function isFlash() { - return $this->request->is('flash'); - } - -/** - * Returns true if the current request is over HTTPS, false otherwise. - * - * @return bool True if call is over HTTPS - * @deprecated 3.0.0 Use `$this->request->is('ssl')` instead. - */ - public function isSSL() { - return $this->request->is('ssl'); - } - -/** - * Returns true if the current call accepts an XML response, false otherwise - * - * @return bool True if client accepts an XML response - */ - public function isXml() { - return $this->prefers('xml'); - } - -/** - * Returns true if the current call accepts an RSS response, false otherwise - * - * @return bool True if client accepts an RSS response - */ - public function isRss() { - return $this->prefers('rss'); - } - -/** - * Returns true if the current call accepts an Atom response, false otherwise - * - * @return bool True if client accepts an RSS response - */ - public function isAtom() { - return $this->prefers('atom'); - } - -/** - * Returns true if user agent string matches a mobile web browser, or if the - * client accepts WAP content. - * - * @return bool True if user agent is a mobile web browser - */ - public function isMobile() { - return $this->request->is('mobile') || $this->accepts('wap'); - } - -/** - * Returns true if the client accepts WAP content - * - * @return bool - */ - public function isWap() { - return $this->prefers('wap'); - } - -/** - * Returns true if the current call a POST request - * - * @return bool True if call is a POST - * @deprecated 3.0.0 Use $this->request->is('post'); from your controller. - */ - public function isPost() { - return $this->request->is('post'); - } - -/** - * Returns true if the current call a PUT request - * - * @return bool True if call is a PUT - * @deprecated 3.0.0 Use $this->request->is('put'); from your controller. - */ - public function isPut() { - return $this->request->is('put'); - } - -/** - * Returns true if the current call a GET request - * - * @return bool True if call is a GET - * @deprecated 3.0.0 Use $this->request->is('get'); from your controller. - */ - public function isGet() { - return $this->request->is('get'); - } - -/** - * Returns true if the current call a DELETE request - * - * @return bool True if call is a DELETE - * @deprecated 3.0.0 Use $this->request->is('delete'); from your controller. - */ - public function isDelete() { - return $this->request->is('delete'); - } - -/** - * Gets Prototype version if call is Ajax, otherwise empty string. - * The Prototype library sets a special "Prototype version" HTTP header. - * - * @return string|bool When Ajax the prototype version of component making the call otherwise false - */ - public function getAjaxVersion() { - $httpX = env('HTTP_X_PROTOTYPE_VERSION'); - return ($httpX === null) ? false : $httpX; - } - -/** - * Adds/sets the Content-type(s) for the given name. This method allows - * content-types to be mapped to friendly aliases (or extensions), which allows - * RequestHandler to automatically respond to requests of that type in the - * startup method. - * - * @param string $name The name of the Content-type, i.e. "html", "xml", "css" - * @param string|array $type The Content-type or array of Content-types assigned to the name, - * i.e. "text/html", or "application/xml" - * @return void - * @deprecated 3.0.0 Use `$this->response->type()` instead. - */ - public function setContent($name, $type = null) { - $this->response->type(array($name => $type)); - } - -/** - * Gets the server name from which this request was referred - * - * @return string Server address - * @deprecated 3.0.0 Use $this->request->referer() from your controller instead - */ - public function getReferer() { - return $this->request->referer(false); - } - -/** - * Gets remote client IP - * - * @param bool $safe Use safe = false when you think the user might manipulate - * their HTTP_CLIENT_IP header. Setting $safe = false will also look at HTTP_X_FORWARDED_FOR - * @return string Client IP address - * @deprecated 3.0.0 Use $this->request->clientIp() from your, controller instead. - */ - public function getClientIP($safe = true) { - return $this->request->clientIp($safe); - } - -/** - * Determines which content types the client accepts. Acceptance is based on - * the file extension parsed by the Router (if present), and by the HTTP_ACCEPT - * header. Unlike CakeRequest::accepts() this method deals entirely with mapped content types. - * - * Usage: - * - * `$this->RequestHandler->accepts(array('xml', 'html', 'json'));` - * - * Returns true if the client accepts any of the supplied types. - * - * `$this->RequestHandler->accepts('xml');` - * - * Returns true if the client accepts xml. - * - * @param string|array $type Can be null (or no parameter), a string type name, or an - * array of types - * @return mixed If null or no parameter is passed, returns an array of content - * types the client accepts. If a string is passed, returns true - * if the client accepts it. If an array is passed, returns true - * if the client accepts one or more elements in the array. - * @see RequestHandlerComponent::setContent() - */ - public function accepts($type = null) { - $accepted = $this->request->accepts(); - - if (!$type) { - return $this->mapType($accepted); - } - if (is_array($type)) { - foreach ($type as $t) { - $t = $this->mapAlias($t); - if (in_array($t, $accepted)) { - return true; - } - } - return false; - } - if (is_string($type)) { - return in_array($this->mapAlias($type), $accepted); - } - return false; - } - -/** - * Determines the content type of the data the client has sent (i.e. in a POST request) - * - * @param string|array $type Can be null (or no parameter), a string type name, or an array of types - * @return mixed If a single type is supplied a boolean will be returned. If no type is provided - * The mapped value of CONTENT_TYPE will be returned. If an array is supplied the first type - * in the request content type will be returned. - */ - public function requestedWith($type = null) { - if ( - !$this->request->is('patch') && - !$this->request->is('post') && - !$this->request->is('put') && - !$this->request->is('delete') - ) { - return null; - } - if (is_array($type)) { - foreach ($type as $t) { - if ($this->requestedWith($t)) { - return $t; - } - } - return false; - } - - list($contentType) = explode(';', env('CONTENT_TYPE')); - if ($contentType === '') { - list($contentType) = explode(';', CakeRequest::header('CONTENT_TYPE')); - } - if (!$type) { - return $this->mapType($contentType); - } - if (is_string($type)) { - return ($type === $this->mapType($contentType)); - } - } - -/** - * Determines which content-types the client prefers. If no parameters are given, - * the single content-type that the client most likely prefers is returned. If $type is - * an array, the first item in the array that the client accepts is returned. - * Preference is determined primarily by the file extension parsed by the Router - * if provided, and secondarily by the list of content-types provided in - * HTTP_ACCEPT. - * - * @param string|array $type An optional array of 'friendly' content-type names, i.e. - * 'html', 'xml', 'js', etc. - * @return mixed If $type is null or not provided, the first content-type in the - * list, based on preference, is returned. If a single type is provided - * a boolean will be returned if that type is preferred. - * If an array of types are provided then the first preferred type is returned. - * If no type is provided the first preferred type is returned. - * @see RequestHandlerComponent::setContent() - */ - public function prefers($type = null) { - $acceptRaw = $this->request->parseAccept(); - - if (empty($acceptRaw)) { - return $this->ext; - } - $accepts = $this->mapType(array_shift($acceptRaw)); - - if (!$type) { - if (empty($this->ext) && !empty($accepts)) { - return $accepts[0]; - } - return $this->ext; - } - - $types = (array)$type; - - if (count($types) === 1) { - if (!empty($this->ext)) { - return in_array($this->ext, $types); - } - return in_array($types[0], $accepts); - } - - $intersect = array_values(array_intersect($accepts, $types)); - if (empty($intersect)) { - return false; - } - return $intersect[0]; - } - -/** - * Sets the layout and template paths for the content type defined by $type. - * - * ### Usage: - * - * Render the response as an 'ajax' response. - * - * `$this->RequestHandler->renderAs($this, 'ajax');` - * - * Render the response as an xml file and force the result as a file download. - * - * `$this->RequestHandler->renderAs($this, 'xml', array('attachment' => 'myfile.xml');` - * - * @param Controller $controller A reference to a controller object - * @param string $type Type of response to send (e.g: 'ajax') - * @param array $options Array of options to use - * @return void - * @see RequestHandlerComponent::setContent() - * @see RequestHandlerComponent::respondAs() - */ - public function renderAs(Controller $controller, $type, $options = array()) { - $defaults = array('charset' => 'UTF-8'); - - if (Configure::read('App.encoding') !== null) { - $defaults['charset'] = Configure::read('App.encoding'); - } - $options += $defaults; - - if ($type === 'ajax') { - $controller->layout = $this->ajaxLayout; - return $this->respondAs('html', $options); - } - - $pluginDot = null; - $viewClassMap = $this->viewClassMap(); - if (array_key_exists($type, $viewClassMap)) { - list($pluginDot, $viewClass) = pluginSplit($viewClassMap[$type], true); - } else { - $viewClass = Inflector::classify($type); - } - $viewName = $viewClass . 'View'; - if (!class_exists($viewName)) { - App::uses($viewName, $pluginDot . 'View'); - } - if (class_exists($viewName)) { - $controller->viewClass = $viewClass; - } elseif (empty($this->_renderType)) { - $controller->viewPath .= DS . $type; - } else { - $controller->viewPath = preg_replace( - "/([\/\\\\]{$this->_renderType})$/", - DS . $type, - $controller->viewPath - ); - } - $this->_renderType = $type; - $controller->layoutPath = $type; - - if ($this->response->getMimeType($type)) { - $this->respondAs($type, $options); - } - - $helper = ucfirst($type); - - if (!in_array($helper, $controller->helpers) && empty($controller->helpers[$helper])) { - App::uses('AppHelper', 'View/Helper'); - App::uses($helper . 'Helper', 'View/Helper'); - if (class_exists($helper . 'Helper')) { - $controller->helpers[] = $helper; - } - } - } - -/** - * Sets the response header based on type map index name. This wraps several methods - * available on CakeResponse. It also allows you to use Content-Type aliases. - * - * @param string|array $type Friendly type name, i.e. 'html' or 'xml', or a full content-type, - * like 'application/x-shockwave'. - * @param array $options If $type is a friendly type name that is associated with - * more than one type of content, $index is used to select which content-type to use. - * @return bool Returns false if the friendly type name given in $type does - * not exist in the type map, or if the Content-type header has - * already been set by this method. - * @see RequestHandlerComponent::setContent() - */ - public function respondAs($type, $options = array()) { - $defaults = array('index' => null, 'charset' => null, 'attachment' => false); - $options = $options + $defaults; - - $cType = $type; - if (strpos($type, '/') === false) { - $cType = $this->response->getMimeType($type); - } - if (is_array($cType)) { - if (isset($cType[$options['index']])) { - $cType = $cType[$options['index']]; - } - - if ($this->prefers($cType)) { - $cType = $this->prefers($cType); - } else { - $cType = $cType[0]; - } - } - - if (!$type) { - return false; - } - if (empty($this->request->params['requested'])) { - $this->response->type($cType); - } - if (!empty($options['charset'])) { - $this->response->charset($options['charset']); - } - if (!empty($options['attachment'])) { - $this->response->download($options['attachment']); - } - return true; - } - -/** - * Returns the current response type (Content-type header), or null if not alias exists - * - * @return mixed A string content type alias, or raw content type if no alias map exists, - * otherwise null - */ - public function responseType() { - return $this->mapType($this->response->type()); - } - -/** - * Maps a content-type back to an alias - * - * @param string|array $cType Either a string content type to map, or an array of types. - * @return string|array Aliases for the types provided. - * @deprecated 3.0.0 Use $this->response->mapType() in your controller instead. - */ - public function mapType($cType) { - return $this->response->mapType($cType); - } - -/** - * Maps a content type alias back to its mime-type(s) - * - * @param string|array $alias String alias to convert back into a content type. Or an array of aliases to map. - * @return string|null Null on an undefined alias. String value of the mapped alias type. If an - * alias maps to more than one content type, the first one will be returned. - */ - public function mapAlias($alias) { - if (is_array($alias)) { - return array_map(array($this, 'mapAlias'), $alias); - } - $type = $this->response->getMimeType($alias); - if ($type) { - if (is_array($type)) { - return $type[0]; - } - return $type; - } - return null; - } - -/** - * Add a new mapped input type. Mapped input types are automatically - * converted by RequestHandlerComponent during the startup() callback. - * - * @param string $type The type alias being converted, ie. json - * @param array $handler The handler array for the type. The first index should - * be the handling callback, all other arguments should be additional parameters - * for the handler. - * @return void - * @throws CakeException - */ - public function addInputType($type, $handler) { - if (!is_array($handler) || !isset($handler[0]) || !is_callable($handler[0])) { - throw new CakeException(__d('cake_dev', 'You must give a handler callback.')); - } - $this->_inputTypeMap[$type] = $handler; - } - -/** - * Getter/setter for viewClassMap - * - * @param array|string $type The type string or array with format `array('type' => 'viewClass')` to map one or more - * @param array $viewClass The viewClass to be used for the type without `View` appended - * @return array|string Returns viewClass when only string $type is set, else array with viewClassMap - */ - public function viewClassMap($type = null, $viewClass = null) { - if (!$viewClass && is_string($type) && isset($this->_viewClassMap[$type])) { - return $this->_viewClassMap[$type]; - } - if (is_string($type)) { - $this->_viewClassMap[$type] = $viewClass; - } elseif (is_array($type)) { - foreach ($type as $key => $value) { - $this->viewClassMap($key, $value); - } - } - return $this->_viewClassMap; - } +class RequestHandlerComponent extends Component +{ + + /** + * The layout that will be switched to for Ajax requests + * + * @var string + * @see RequestHandler::setAjax() + */ + public $ajaxLayout = 'ajax'; + + /** + * Determines whether or not callbacks will be fired on this component + * + * @var bool + */ + public $enabled = true; + + /** + * Holds the reference to Controller::$request + * + * @var CakeRequest + */ + public $request; + + /** + * Holds the reference to Controller::$response + * + * @var CakeResponse + */ + public $response; + + /** + * Contains the file extension parsed out by the Router + * + * @var string + * @see Router::parseExtensions() + */ + public $ext = null; + + /** + * Array of parameters parsed from the URL. + * + * @var array|null + */ + public $params = null; + + /** + * The template to use when rendering the given content type. + * + * @var string + */ + protected $_renderType = null; + + /** + * A mapping between extensions and deserializers for request bodies of that type. + * By default only JSON and XML are mapped, use RequestHandlerComponent::addInputType() + * + * @var array + */ + protected $_inputTypeMap = [ + 'json' => ['json_decode', true] + ]; + + /** + * A mapping between type and viewClass + * By default only JSON and XML are mapped, use RequestHandlerComponent::viewClassMap() + * + * @var array + */ + protected $_viewClassMap = [ + 'json' => 'Json', + 'xml' => 'Xml' + ]; + + /** + * Constructor. Parses the accepted content types accepted by the client using HTTP_ACCEPT + * + * @param ComponentCollection $collection ComponentCollection object. + * @param array $settings Array of settings. + */ + public function __construct(ComponentCollection $collection, $settings = []) + { + parent::__construct($collection, $settings + ['checkHttpCache' => true]); + $this->addInputType('xml', [[$this, 'convertXml']]); + + $Controller = $collection->getController(); + $this->request = $Controller->request; + $this->response = $Controller->response; + } + + /** + * Add a new mapped input type. Mapped input types are automatically + * converted by RequestHandlerComponent during the startup() callback. + * + * @param string $type The type alias being converted, ie. json + * @param array $handler The handler array for the type. The first index should + * be the handling callback, all other arguments should be additional parameters + * for the handler. + * @return void + * @throws CakeException + */ + public function addInputType($type, $handler) + { + if (!is_array($handler) || !isset($handler[0]) || !is_callable($handler[0])) { + throw new CakeException(__d('cake_dev', 'You must give a handler callback.')); + } + $this->_inputTypeMap[$type] = $handler; + } + + /** + * Checks to see if a file extension has been parsed by the Router, or if the + * HTTP_ACCEPT_TYPE has matches only one content type with the supported extensions. + * If there is only one matching type between the supported content types & extensions, + * and the requested mime-types, RequestHandler::$ext is set to that value. + * + * @param Controller $controller A reference to the controller + * @return void + * @see Router::parseExtensions() + */ + public function initialize(Controller $controller) + { + if (isset($this->request->params['ext'])) { + $this->ext = $this->request->params['ext']; + } + if (empty($this->ext) || $this->ext === 'html') { + $this->_setExtension(); + } + $this->params = $controller->request->params; + if (!empty($this->settings['viewClassMap'])) { + $this->viewClassMap($this->settings['viewClassMap']); + } + } + + /** + * Set the extension based on the accept headers. + * Compares the accepted types and configured extensions. + * If there is one common type, that is assigned as the ext/content type + * for the response. + * Type with the highest weight will be set. If the highest weight has more + * then one type matching the extensions, the order in which extensions are specified + * determines which type will be set. + * + * If html is one of the preferred types, no content type will be set, this + * is to avoid issues with browsers that prefer html and several other content types. + * + * @return void + */ + protected function _setExtension() + { + $accept = $this->request->parseAccept(); + if (empty($accept)) { + return; + } + + $accepts = $this->response->mapType($accept); + $preferedTypes = current($accepts); + if (array_intersect($preferedTypes, ['html', 'xhtml'])) { + return; + } + + $extensions = Router::extensions(); + foreach ($accepts as $types) { + $ext = array_intersect($extensions, $types); + if ($ext) { + $this->ext = current($ext); + break; + } + } + } + + /** + * Getter/setter for viewClassMap + * + * @param array|string $type The type string or array with format `array('type' => 'viewClass')` to map one or more + * @param array $viewClass The viewClass to be used for the type without `View` appended + * @return array|string Returns viewClass when only string $type is set, else array with viewClassMap + */ + public function viewClassMap($type = null, $viewClass = null) + { + if (!$viewClass && is_string($type) && isset($this->_viewClassMap[$type])) { + return $this->_viewClassMap[$type]; + } + if (is_string($type)) { + $this->_viewClassMap[$type] = $viewClass; + } else if (is_array($type)) { + foreach ($type as $key => $value) { + $this->viewClassMap($key, $value); + } + } + return $this->_viewClassMap; + } + + /** + * The startup method of the RequestHandler enables several automatic behaviors + * related to the detection of certain properties of the HTTP request, including: + * + * - Disabling layout rendering for Ajax requests (based on the HTTP_X_REQUESTED_WITH header) + * - If Router::parseExtensions() is enabled, the layout and template type are + * switched based on the parsed extension or Accept-Type header. For example, if `controller/action.xml` + * is requested, the view path becomes `app/View/Controller/xml/action.ctp`. Also if + * `controller/action` is requested with `Accept-Type: application/xml` in the headers + * the view path will become `app/View/Controller/xml/action.ctp`. Layout and template + * types will only switch to mime-types recognized by CakeResponse. If you need to declare + * additional mime-types, you can do so using CakeResponse::type() in your controllers beforeFilter() + * method. + * - If a helper with the same name as the extension exists, it is added to the controller. + * - If the extension is of a type that RequestHandler understands, it will set that + * Content-type in the response header. + * - If the XML data is POSTed, the data is parsed into an XML object, which is assigned + * to the $data property of the controller, which can then be saved to a model object. + * + * @param Controller $controller A reference to the controller + * @return void + */ + public function startup(Controller $controller) + { + $controller->request->params['isAjax'] = $this->request->is('ajax'); + $isRecognized = ( + !in_array($this->ext, ['html', 'htm']) && + $this->response->getMimeType($this->ext) + ); + + if (!empty($this->ext) && $isRecognized) { + $this->renderAs($controller, $this->ext); + } else if ($this->request->is('ajax')) { + $this->renderAs($controller, 'ajax'); + } else if (empty($this->ext) || in_array($this->ext, ['html', 'htm'])) { + $this->respondAs('html', ['charset' => Configure::read('App.encoding')]); + } + + foreach ($this->_inputTypeMap as $type => $handler) { + if ($this->requestedWith($type)) { + $input = (array)call_user_func_array([$controller->request, 'input'], $handler); + $controller->request->data = $input; + } + } + } + + /** + * Sets the layout and template paths for the content type defined by $type. + * + * ### Usage: + * + * Render the response as an 'ajax' response. + * + * `$this->RequestHandler->renderAs($this, 'ajax');` + * + * Render the response as an xml file and force the result as a file download. + * + * `$this->RequestHandler->renderAs($this, 'xml', array('attachment' => 'myfile.xml');` + * + * @param Controller $controller A reference to a controller object + * @param string $type Type of response to send (e.g: 'ajax') + * @param array $options Array of options to use + * @return void + * @see RequestHandlerComponent::setContent() + * @see RequestHandlerComponent::respondAs() + */ + public function renderAs(Controller $controller, $type, $options = []) + { + $defaults = ['charset' => 'UTF-8']; + + if (Configure::read('App.encoding') !== null) { + $defaults['charset'] = Configure::read('App.encoding'); + } + $options += $defaults; + + if ($type === 'ajax') { + $controller->layout = $this->ajaxLayout; + return $this->respondAs('html', $options); + } + + $pluginDot = null; + $viewClassMap = $this->viewClassMap(); + if (array_key_exists($type, $viewClassMap)) { + list($pluginDot, $viewClass) = pluginSplit($viewClassMap[$type], true); + } else { + $viewClass = Inflector::classify($type); + } + $viewName = $viewClass . 'View'; + if (!class_exists($viewName)) { + App::uses($viewName, $pluginDot . 'View'); + } + if (class_exists($viewName)) { + $controller->viewClass = $viewClass; + } else if (empty($this->_renderType)) { + $controller->viewPath .= DS . $type; + } else { + $controller->viewPath = preg_replace( + "/([\/\\\\]{$this->_renderType})$/", + DS . $type, + $controller->viewPath + ); + } + $this->_renderType = $type; + $controller->layoutPath = $type; + + if ($this->response->getMimeType($type)) { + $this->respondAs($type, $options); + } + + $helper = ucfirst($type); + + if (!in_array($helper, $controller->helpers) && empty($controller->helpers[$helper])) { + App::uses('AppHelper', 'View/Helper'); + App::uses($helper . 'Helper', 'View/Helper'); + if (class_exists($helper . 'Helper')) { + $controller->helpers[] = $helper; + } + } + } + + /** + * Sets the response header based on type map index name. This wraps several methods + * available on CakeResponse. It also allows you to use Content-Type aliases. + * + * @param string|array $type Friendly type name, i.e. 'html' or 'xml', or a full content-type, + * like 'application/x-shockwave'. + * @param array $options If $type is a friendly type name that is associated with + * more than one type of content, $index is used to select which content-type to use. + * @return bool Returns false if the friendly type name given in $type does + * not exist in the type map, or if the Content-type header has + * already been set by this method. + * @see RequestHandlerComponent::setContent() + */ + public function respondAs($type, $options = []) + { + $defaults = ['index' => null, 'charset' => null, 'attachment' => false]; + $options = $options + $defaults; + + $cType = $type; + if (strpos($type, '/') === false) { + $cType = $this->response->getMimeType($type); + } + if (is_array($cType)) { + if (isset($cType[$options['index']])) { + $cType = $cType[$options['index']]; + } + + if ($this->prefers($cType)) { + $cType = $this->prefers($cType); + } else { + $cType = $cType[0]; + } + } + + if (!$type) { + return false; + } + if (empty($this->request->params['requested'])) { + $this->response->type($cType); + } + if (!empty($options['charset'])) { + $this->response->charset($options['charset']); + } + if (!empty($options['attachment'])) { + $this->response->download($options['attachment']); + } + return true; + } + + /** + * Determines which content-types the client prefers. If no parameters are given, + * the single content-type that the client most likely prefers is returned. If $type is + * an array, the first item in the array that the client accepts is returned. + * Preference is determined primarily by the file extension parsed by the Router + * if provided, and secondarily by the list of content-types provided in + * HTTP_ACCEPT. + * + * @param string|array $type An optional array of 'friendly' content-type names, i.e. + * 'html', 'xml', 'js', etc. + * @return mixed If $type is null or not provided, the first content-type in the + * list, based on preference, is returned. If a single type is provided + * a boolean will be returned if that type is preferred. + * If an array of types are provided then the first preferred type is returned. + * If no type is provided the first preferred type is returned. + * @see RequestHandlerComponent::setContent() + */ + public function prefers($type = null) + { + $acceptRaw = $this->request->parseAccept(); + + if (empty($acceptRaw)) { + return $this->ext; + } + $accepts = $this->mapType(array_shift($acceptRaw)); + + if (!$type) { + if (empty($this->ext) && !empty($accepts)) { + return $accepts[0]; + } + return $this->ext; + } + + $types = (array)$type; + + if (count($types) === 1) { + if (!empty($this->ext)) { + return in_array($this->ext, $types); + } + return in_array($types[0], $accepts); + } + + $intersect = array_values(array_intersect($accepts, $types)); + if (empty($intersect)) { + return false; + } + return $intersect[0]; + } + + /** + * Maps a content-type back to an alias + * + * @param string|array $cType Either a string content type to map, or an array of types. + * @return string|array Aliases for the types provided. + * @deprecated 3.0.0 Use $this->response->mapType() in your controller instead. + */ + public function mapType($cType) + { + return $this->response->mapType($cType); + } + + /** + * Determines the content type of the data the client has sent (i.e. in a POST request) + * + * @param string|array $type Can be null (or no parameter), a string type name, or an array of types + * @return mixed If a single type is supplied a boolean will be returned. If no type is provided + * The mapped value of CONTENT_TYPE will be returned. If an array is supplied the first type + * in the request content type will be returned. + */ + public function requestedWith($type = null) + { + if ( + !$this->request->is('patch') && + !$this->request->is('post') && + !$this->request->is('put') && + !$this->request->is('delete') + ) { + return null; + } + if (is_array($type)) { + foreach ($type as $t) { + if ($this->requestedWith($t)) { + return $t; + } + } + return false; + } + + list($contentType) = explode(';', env('CONTENT_TYPE')); + if ($contentType === '') { + list($contentType) = explode(';', CakeRequest::header('CONTENT_TYPE')); + } + if (!$type) { + return $this->mapType($contentType); + } + if (is_string($type)) { + return ($type === $this->mapType($contentType)); + } + } + + /** + * Helper method to parse xml input data, due to lack of anonymous functions + * this lives here. + * + * @param string $xml XML string. + * @return array Xml array data + */ + public function convertXml($xml) + { + try { + $xml = Xml::build($xml, ['readFile' => false]); + if (isset($xml->data)) { + return Xml::toArray($xml->data); + } + return Xml::toArray($xml); + } catch (XmlException $e) { + return []; + } + } + + /** + * Handles (fakes) redirects for Ajax requests using requestAction() + * Modifies the $_POST and $_SERVER['REQUEST_METHOD'] to simulate a new GET request. + * + * @param Controller $controller A reference to the controller + * @param string|array $url A string or array containing the redirect location + * @param int|array $status HTTP Status for redirect + * @param bool $exit Whether to exit script, defaults to `true`. + * @return void + */ + public function beforeRedirect(Controller $controller, $url, $status = null, $exit = true) + { + if (!$this->request->is('ajax')) { + return; + } + if (empty($url)) { + return; + } + $_SERVER['REQUEST_METHOD'] = 'GET'; + foreach ($_POST as $key => $val) { + unset($_POST[$key]); + } + if (is_array($url)) { + $url = Router::url($url + ['base' => false]); + } + if (!empty($status)) { + $statusCode = $this->response->httpCodes($status); + if (is_array($statusCode)) { + $code = key($statusCode); + $this->response->statusCode($code); + } + } + $this->response->body($this->requestAction($url, ['return', 'bare' => false])); + $this->response->send(); + $this->_stop(); + } + + /** + * Checks if the response can be considered different according to the request + * headers, and the caching response headers. If it was not modified, then the + * render process is skipped. And the client will get a blank response with a + * "304 Not Modified" header. + * + * @param Controller $controller Controller instance. + * @return bool False if the render process should be aborted. + */ + public function beforeRender(Controller $controller) + { + if ($this->settings['checkHttpCache'] && $this->response->checkNotModified($this->request)) { + return false; + } + } + + /** + * Returns true if the current HTTP request is Ajax, false otherwise + * + * @return bool True if call is Ajax + * @deprecated 3.0.0 Use `$this->request->is('ajax')` instead. + */ + public function isAjax() + { + return $this->request->is('ajax'); + } + + /** + * Returns true if the current HTTP request is coming from a Flash-based client + * + * @return bool True if call is from Flash + * @deprecated 3.0.0 Use `$this->request->is('flash')` instead. + */ + public function isFlash() + { + return $this->request->is('flash'); + } + + /** + * Returns true if the current request is over HTTPS, false otherwise. + * + * @return bool True if call is over HTTPS + * @deprecated 3.0.0 Use `$this->request->is('ssl')` instead. + */ + public function isSSL() + { + return $this->request->is('ssl'); + } + + /** + * Returns true if the current call accepts an XML response, false otherwise + * + * @return bool True if client accepts an XML response + */ + public function isXml() + { + return $this->prefers('xml'); + } + + /** + * Returns true if the current call accepts an RSS response, false otherwise + * + * @return bool True if client accepts an RSS response + */ + public function isRss() + { + return $this->prefers('rss'); + } + + /** + * Returns true if the current call accepts an Atom response, false otherwise + * + * @return bool True if client accepts an RSS response + */ + public function isAtom() + { + return $this->prefers('atom'); + } + + /** + * Returns true if user agent string matches a mobile web browser, or if the + * client accepts WAP content. + * + * @return bool True if user agent is a mobile web browser + */ + public function isMobile() + { + return $this->request->is('mobile') || $this->accepts('wap'); + } + + /** + * Determines which content types the client accepts. Acceptance is based on + * the file extension parsed by the Router (if present), and by the HTTP_ACCEPT + * header. Unlike CakeRequest::accepts() this method deals entirely with mapped content types. + * + * Usage: + * + * `$this->RequestHandler->accepts(array('xml', 'html', 'json'));` + * + * Returns true if the client accepts any of the supplied types. + * + * `$this->RequestHandler->accepts('xml');` + * + * Returns true if the client accepts xml. + * + * @param string|array $type Can be null (or no parameter), a string type name, or an + * array of types + * @return mixed If null or no parameter is passed, returns an array of content + * types the client accepts. If a string is passed, returns true + * if the client accepts it. If an array is passed, returns true + * if the client accepts one or more elements in the array. + * @see RequestHandlerComponent::setContent() + */ + public function accepts($type = null) + { + $accepted = $this->request->accepts(); + + if (!$type) { + return $this->mapType($accepted); + } + if (is_array($type)) { + foreach ($type as $t) { + $t = $this->mapAlias($t); + if (in_array($t, $accepted)) { + return true; + } + } + return false; + } + if (is_string($type)) { + return in_array($this->mapAlias($type), $accepted); + } + return false; + } + + /** + * Maps a content type alias back to its mime-type(s) + * + * @param string|array $alias String alias to convert back into a content type. Or an array of aliases to map. + * @return string|null Null on an undefined alias. String value of the mapped alias type. If an + * alias maps to more than one content type, the first one will be returned. + */ + public function mapAlias($alias) + { + if (is_array($alias)) { + return array_map([$this, 'mapAlias'], $alias); + } + $type = $this->response->getMimeType($alias); + if ($type) { + if (is_array($type)) { + return $type[0]; + } + return $type; + } + return null; + } + + /** + * Returns true if the client accepts WAP content + * + * @return bool + */ + public function isWap() + { + return $this->prefers('wap'); + } + + /** + * Returns true if the current call a POST request + * + * @return bool True if call is a POST + * @deprecated 3.0.0 Use $this->request->is('post'); from your controller. + */ + public function isPost() + { + return $this->request->is('post'); + } + + /** + * Returns true if the current call a PUT request + * + * @return bool True if call is a PUT + * @deprecated 3.0.0 Use $this->request->is('put'); from your controller. + */ + public function isPut() + { + return $this->request->is('put'); + } + + /** + * Returns true if the current call a GET request + * + * @return bool True if call is a GET + * @deprecated 3.0.0 Use $this->request->is('get'); from your controller. + */ + public function isGet() + { + return $this->request->is('get'); + } + + /** + * Returns true if the current call a DELETE request + * + * @return bool True if call is a DELETE + * @deprecated 3.0.0 Use $this->request->is('delete'); from your controller. + */ + public function isDelete() + { + return $this->request->is('delete'); + } + + /** + * Gets Prototype version if call is Ajax, otherwise empty string. + * The Prototype library sets a special "Prototype version" HTTP header. + * + * @return string|bool When Ajax the prototype version of component making the call otherwise false + */ + public function getAjaxVersion() + { + $httpX = env('HTTP_X_PROTOTYPE_VERSION'); + return ($httpX === null) ? false : $httpX; + } + + /** + * Adds/sets the Content-type(s) for the given name. This method allows + * content-types to be mapped to friendly aliases (or extensions), which allows + * RequestHandler to automatically respond to requests of that type in the + * startup method. + * + * @param string $name The name of the Content-type, i.e. "html", "xml", "css" + * @param string|array $type The Content-type or array of Content-types assigned to the name, + * i.e. "text/html", or "application/xml" + * @return void + * @deprecated 3.0.0 Use `$this->response->type()` instead. + */ + public function setContent($name, $type = null) + { + $this->response->type([$name => $type]); + } + + /** + * Gets the server name from which this request was referred + * + * @return string Server address + * @deprecated 3.0.0 Use $this->request->referer() from your controller instead + */ + public function getReferer() + { + return $this->request->referer(false); + } + + /** + * Gets remote client IP + * + * @param bool $safe Use safe = false when you think the user might manipulate + * their HTTP_CLIENT_IP header. Setting $safe = false will also look at HTTP_X_FORWARDED_FOR + * @return string Client IP address + * @deprecated 3.0.0 Use $this->request->clientIp() from your, controller instead. + */ + public function getClientIP($safe = true) + { + return $this->request->clientIp($safe); + } + + /** + * Returns the current response type (Content-type header), or null if not alias exists + * + * @return mixed A string content type alias, or raw content type if no alias map exists, + * otherwise null + */ + public function responseType() + { + return $this->mapType($this->response->type()); + } } diff --git a/lib/Cake/Controller/Component/SecurityComponent.php b/lib/Cake/Controller/Component/SecurityComponent.php index 308f27c7..72a04f10 100755 --- a/lib/Cake/Controller/Component/SecurityComponent.php +++ b/lib/Cake/Controller/Component/SecurityComponent.php @@ -34,856 +34,882 @@ * @package Cake.Controller.Component * @link https://book.cakephp.org/2.0/en/core-libraries/components/security-component.html */ -class SecurityComponent extends Component { - -/** - * Default message used for exceptions thrown - */ - const DEFAULT_EXCEPTION_MESSAGE = 'The request has been black-holed'; - -/** - * The controller method that will be called if this request is black-hole'd - * - * @var string - */ - public $blackHoleCallback = null; - -/** - * List of controller actions for which a POST request is required - * - * @var array - * @deprecated 3.0.0 Use CakeRequest::allowMethod() instead. - * @see SecurityComponent::requirePost() - */ - public $requirePost = array(); - -/** - * List of controller actions for which a GET request is required - * - * @var array - * @deprecated 3.0.0 Use CakeRequest::allowMethod() instead. - * @see SecurityComponent::requireGet() - */ - public $requireGet = array(); - -/** - * List of controller actions for which a PUT request is required - * - * @var array - * @deprecated 3.0.0 Use CakeRequest::allowMethod() instead. - * @see SecurityComponent::requirePut() - */ - public $requirePut = array(); - -/** - * List of controller actions for which a DELETE request is required - * - * @var array - * @deprecated 3.0.0 Use CakeRequest::allowMethod() instead. - * @see SecurityComponent::requireDelete() - */ - public $requireDelete = array(); - -/** - * List of actions that require an SSL-secured connection - * - * @var array - * @see SecurityComponent::requireSecure() - */ - public $requireSecure = array(); - -/** - * List of actions that require a valid authentication key - * - * @var array - * @see SecurityComponent::requireAuth() - * @deprecated 2.8.1 This feature is confusing and not useful. - */ - public $requireAuth = array(); - -/** - * Controllers from which actions of the current controller are allowed to receive - * requests. - * - * @var array - * @see SecurityComponent::requireAuth() - */ - public $allowedControllers = array(); - -/** - * Actions from which actions of the current controller are allowed to receive - * requests. - * - * @var array - * @see SecurityComponent::requireAuth() - */ - public $allowedActions = array(); - -/** - * Deprecated property, superseded by unlockedFields. - * - * @var array - * @deprecated 3.0.0 Superseded by unlockedFields. - * @see SecurityComponent::$unlockedFields - */ - public $disabledFields = array(); - -/** - * Form fields to exclude from POST validation. Fields can be unlocked - * either in the Component, or with FormHelper::unlockField(). - * Fields that have been unlocked are not required to be part of the POST - * and hidden unlocked fields do not have their values checked. - * - * @var array - */ - public $unlockedFields = array(); - -/** - * Actions to exclude from CSRF and POST validation checks. - * Other checks like requireAuth(), requireSecure(), - * requirePost(), requireGet() etc. will still be applied. - * - * @var array - */ - public $unlockedActions = array(); - -/** - * Whether to validate POST data. Set to false to disable for data coming from 3rd party - * services, etc. - * - * @var bool - */ - public $validatePost = true; - -/** - * Whether to use CSRF protected forms. Set to false to disable CSRF protection on forms. - * - * @var bool - * @see http://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF) - * @see SecurityComponent::$csrfExpires - */ - public $csrfCheck = true; - -/** - * The duration from when a CSRF token is created that it will expire on. - * Each form/page request will generate a new token that can only be submitted once unless - * it expires. Can be any value compatible with strtotime() - * - * @var string - */ - public $csrfExpires = '+30 minutes'; - -/** - * Controls whether or not CSRF tokens are use and burn. Set to false to not generate - * new tokens on each request. One token will be reused until it expires. This reduces - * the chances of users getting invalid requests because of token consumption. - * It has the side effect of making CSRF less secure, as tokens are reusable. - * - * @var bool - */ - public $csrfUseOnce = true; - -/** - * Control the number of tokens a user can keep open. - * This is most useful with one-time use tokens. Since new tokens - * are created on each request, having a hard limit on the number of open tokens - * can be useful in controlling the size of the session file. - * - * When tokens are evicted, the oldest ones will be removed, as they are the most likely - * to be dead/expired. - * - * @var int - */ - public $csrfLimit = 100; - -/** - * Other components used by the Security component - * - * @var array - */ - public $components = array('Session'); - -/** - * Holds the current action of the controller - * - * @var string - */ - protected $_action = null; - -/** - * Request object - * - * @var CakeRequest - */ - public $request; - -/** - * Component startup. All security checking happens here. - * - * @param Controller $controller Instantiating controller - * @throws AuthSecurityException - * @return void - */ - public function startup(Controller $controller) { - $this->request = $controller->request; - $this->_action = $controller->request->params['action']; - $hasData = ($controller->request->data || $controller->request->is(array('put', 'post', 'delete', 'patch'))); - try { - $this->_methodsRequired($controller); - $this->_secureRequired($controller); - $this->_authRequired($controller); - - $isNotRequestAction = ( - !isset($controller->request->params['requested']) || - $controller->request->params['requested'] != 1 - ); - - if ($this->_action === $this->blackHoleCallback) { - throw new AuthSecurityException(sprintf('Action %s is defined as the blackhole callback.', $this->_action)); - } - - if (!in_array($this->_action, (array)$this->unlockedActions) && $hasData && $isNotRequestAction) { - if ($this->validatePost) { - $this->_validatePost($controller); - } - if ($this->csrfCheck) { - $this->_validateCsrf($controller); - } - } - - } catch (SecurityException $se) { - return $this->blackHole($controller, $se->getType(), $se); - } - - $this->generateToken($controller->request); - if ($hasData && is_array($controller->request->data)) { - unset($controller->request->data['_Token']); - } - } - -/** - * Sets the actions that require a POST request, or empty for all actions - * - * @return void - * @deprecated 3.0.0 Use CakeRequest::onlyAllow() instead. - * @link https://book.cakephp.org/2.0/en/core-libraries/components/security-component.html#SecurityComponent::requirePost - */ - public function requirePost() { - $args = func_get_args(); - $this->_requireMethod('Post', $args); - } - -/** - * Sets the actions that require a GET request, or empty for all actions - * - * @deprecated 3.0.0 Use CakeRequest::onlyAllow() instead. - * @return void - */ - public function requireGet() { - $args = func_get_args(); - $this->_requireMethod('Get', $args); - } - -/** - * Sets the actions that require a PUT request, or empty for all actions - * - * @deprecated 3.0.0 Use CakeRequest::onlyAllow() instead. - * @return void - */ - public function requirePut() { - $args = func_get_args(); - $this->_requireMethod('Put', $args); - } - -/** - * Sets the actions that require a DELETE request, or empty for all actions - * - * @deprecated 3.0.0 Use CakeRequest::onlyAllow() instead. - * @return void - */ - public function requireDelete() { - $args = func_get_args(); - $this->_requireMethod('Delete', $args); - } - -/** - * Sets the actions that require a request that is SSL-secured, or empty for all actions - * - * @return void - * @link https://book.cakephp.org/2.0/en/core-libraries/components/security-component.html#SecurityComponent::requireSecure - */ - public function requireSecure() { - $args = func_get_args(); - $this->_requireMethod('Secure', $args); - } - -/** - * Sets the actions that require whitelisted form submissions. - * - * Adding actions with this method will enforce the restrictions - * set in SecurityComponent::$allowedControllers and - * SecurityComponent::$allowedActions. - * - * @return void - * @link https://book.cakephp.org/2.0/en/core-libraries/components/security-component.html#SecurityComponent::requireAuth - */ - public function requireAuth() { - $args = func_get_args(); - $this->_requireMethod('Auth', $args); - } - -/** - * Black-hole an invalid request with a 400 error or custom callback. If SecurityComponent::$blackHoleCallback - * is specified, it will use this callback by executing the method indicated in $error - * - * @param Controller $controller Instantiating controller - * @param string $error Error method - * @param SecurityException|null $exception Additional debug info describing the cause - * @return mixed If specified, controller blackHoleCallback's response, or no return otherwise - * @see SecurityComponent::$blackHoleCallback - * @link https://book.cakephp.org/2.0/en/core-libraries/components/security-component.html#handling-blackhole-callbacks - * @throws BadRequestException - */ - public function blackHole(Controller $controller, $error = '', SecurityException $exception = null) { - if (!$this->blackHoleCallback) { - $this->_throwException($exception); - } - return $this->_callback($controller, $this->blackHoleCallback, array($error)); - } - -/** - * Check debug status and throw an Exception based on the existing one - * - * @param SecurityException|null $exception Additional debug info describing the cause - * @throws BadRequestException - * @return void - */ - protected function _throwException($exception = null) { - if ($exception !== null) { - if (!Configure::read('debug') && $exception instanceof SecurityException) { - $exception->setReason($exception->getMessage()); - $exception->setMessage(self::DEFAULT_EXCEPTION_MESSAGE); - } - throw $exception; - } - throw new BadRequestException(self::DEFAULT_EXCEPTION_MESSAGE); - } - -/** - * Sets the actions that require a $method HTTP request, or empty for all actions - * - * @param string $method The HTTP method to assign controller actions to - * @param array $actions Controller actions to set the required HTTP method to. - * @return void - */ - protected function _requireMethod($method, $actions = array()) { - if (isset($actions[0]) && is_array($actions[0])) { - $actions = $actions[0]; - } - $this->{'require' . $method} = (empty($actions)) ? array('*') : $actions; - } - -/** - * Check if HTTP methods are required - * - * @param Controller $controller Instantiating controller - * @throws SecurityException - * @return bool True if $method is required - */ - protected function _methodsRequired(Controller $controller) { - foreach (array('Post', 'Get', 'Put', 'Delete') as $method) { - $property = 'require' . $method; - if (is_array($this->$property) && !empty($this->$property)) { - $require = $this->$property; - if (in_array($this->_action, $require) || $this->$property === array('*')) { - if (!$controller->request->is($method)) { - throw new SecurityException( - sprintf('The request method must be %s', strtoupper($method)) - ); - } - } - } - } - return true; - } - -/** - * Check if access requires secure connection - * - * @param Controller $controller Instantiating controller - * @throws SecurityException - * @return bool True if secure connection required - */ - protected function _secureRequired(Controller $controller) { - if (is_array($this->requireSecure) && !empty($this->requireSecure)) { - $requireSecure = $this->requireSecure; - - if (in_array($this->_action, $requireSecure) || $this->requireSecure === array('*')) { - if (!$controller->request->is('ssl')) { - throw new SecurityException( - 'Request is not SSL and the action is required to be secure' - ); - } - } - } - return true; - } - -/** - * Check if authentication is required - * - * @param Controller $controller Instantiating controller - * @return bool|null True if authentication required - * @throws AuthSecurityException - * @deprecated 2.8.1 This feature is confusing and not useful. - */ - protected function _authRequired(Controller $controller) { - if (is_array($this->requireAuth) && !empty($this->requireAuth) && !empty($controller->request->data)) { - $requireAuth = $this->requireAuth; - - if (in_array($controller->request->params['action'], $requireAuth) || $this->requireAuth === array('*')) { - if (!isset($controller->request->data['_Token'])) { - throw new AuthSecurityException('\'_Token\' was not found in request data.'); - } - - if ($this->Session->check('_Token')) { - $tData = $this->Session->read('_Token'); - - if (!empty($tData['allowedControllers']) && - !in_array($controller->request->params['controller'], $tData['allowedControllers'])) { - throw new AuthSecurityException( - sprintf( - 'Controller \'%s\' was not found in allowed controllers: \'%s\'.', - $controller->request->params['controller'], - implode(', ', (array)$tData['allowedControllers']) - ) - ); - } - if (!empty($tData['allowedActions']) && - !in_array($controller->request->params['action'], $tData['allowedActions']) - ) { - throw new AuthSecurityException( - sprintf( - 'Action \'%s::%s\' was not found in allowed actions: \'%s\'.', - $controller->request->params['controller'], - $controller->request->params['action'], - implode(', ', (array)$tData['allowedActions']) - ) - ); - } - } else { - throw new AuthSecurityException('\'_Token\' was not found in session.'); - } - } - } - return true; - } - -/** - * Validate submitted form - * - * @param Controller $controller Instantiating controller - * @throws AuthSecurityException - * @return bool true if submitted form is valid - */ - protected function _validatePost(Controller $controller) { - $token = $this->_validToken($controller); - $hashParts = $this->_hashParts($controller); - $check = Security::hash(implode('', $hashParts), 'sha1'); - - if ($token === $check) { - return true; - } - - $msg = self::DEFAULT_EXCEPTION_MESSAGE; - if (Configure::read('debug')) { - $msg = $this->_debugPostTokenNotMatching($controller, $hashParts); - } - - throw new AuthSecurityException($msg); - } - -/** - * Check if token is valid - * - * @param Controller $controller Instantiating controller - * @throws AuthSecurityException - * @throws SecurityException - * @return string fields token - */ - protected function _validToken(Controller $controller) { - $check = $controller->request->data; - - $message = '\'%s\' was not found in request data.'; - if (!isset($check['_Token'])) { - throw new AuthSecurityException(sprintf($message, '_Token')); - } - if (!isset($check['_Token']['fields'])) { - throw new AuthSecurityException(sprintf($message, '_Token.fields')); - } - if (!isset($check['_Token']['unlocked'])) { - throw new AuthSecurityException(sprintf($message, '_Token.unlocked')); - } - if (Configure::read('debug') && !isset($check['_Token']['debug'])) { - throw new SecurityException(sprintf($message, '_Token.debug')); - } - if (!Configure::read('debug') && isset($check['_Token']['debug'])) { - throw new SecurityException('Unexpected \'_Token.debug\' found in request data'); - } - - $token = urldecode($check['_Token']['fields']); - if (strpos($token, ':')) { - list($token, ) = explode(':', $token, 2); - } - - return $token; - } - -/** - * Return hash parts for the Token generation - * - * @param Controller $controller Instantiating controller - * @return array - */ - protected function _hashParts(Controller $controller) { - $fieldList = $this->_fieldsList($controller->request->data); - $unlocked = $this->_sortedUnlocked($controller->request->data); - - return array( - $controller->request->here(), - serialize($fieldList), - $unlocked, - Configure::read('Security.salt') - ); - } - -/** - * Return the fields list for the hash calculation - * - * @param array $check Data array - * @return array - */ - protected function _fieldsList(array $check) { - $locked = ''; - $token = urldecode($check['_Token']['fields']); - $unlocked = $this->_unlocked($check); - - if (strpos($token, ':')) { - list($token, $locked) = explode(':', $token, 2); - } - unset($check['_Token'], $check['_csrfToken']); - - $locked = explode('|', $locked); - $unlocked = explode('|', $unlocked); - - $fields = Hash::flatten($check); - $fieldList = array_keys($fields); - $multi = $lockedFields = array(); - $isUnlocked = false; - - foreach ($fieldList as $i => $key) { - if (preg_match('/(\.\d+){1,10}$/', $key)) { - $multi[$i] = preg_replace('/(\.\d+){1,10}$/', '', $key); - unset($fieldList[$i]); - } else { - $fieldList[$i] = (string)$key; - } - } - if (!empty($multi)) { - $fieldList += array_unique($multi); - } - - $unlockedFields = array_unique( - array_merge((array)$this->disabledFields, (array)$this->unlockedFields, $unlocked) - ); - - foreach ($fieldList as $i => $key) { - $isLocked = (is_array($locked) && in_array($key, $locked)); - - if (!empty($unlockedFields)) { - foreach ($unlockedFields as $off) { - $off = explode('.', $off); - $field = array_values(array_intersect(explode('.', $key), $off)); - $isUnlocked = ($field === $off); - if ($isUnlocked) { - break; - } - } - } - - if ($isUnlocked || $isLocked) { - unset($fieldList[$i]); - if ($isLocked) { - $lockedFields[$key] = $fields[$key]; - } - } - } - sort($fieldList, SORT_STRING); - ksort($lockedFields, SORT_STRING); - $fieldList += $lockedFields; - - return $fieldList; - } - -/** - * Get the unlocked string - * - * @param array $data Data array - * @return string - */ - protected function _unlocked(array $data) { - return urldecode($data['_Token']['unlocked']); - } - -/** - * Get the sorted unlocked string - * - * @param array $data Data array - * @return string - */ - protected function _sortedUnlocked($data) { - $unlocked = $this->_unlocked($data); - $unlocked = explode('|', $unlocked); - sort($unlocked, SORT_STRING); - - return implode('|', $unlocked); - } - -/** - * Create a message for humans to understand why Security token is not matching - * - * @param Controller $controller Instantiating controller - * @param array $hashParts Elements used to generate the Token hash - * @return string Message explaining why the tokens are not matching - */ - protected function _debugPostTokenNotMatching(Controller $controller, $hashParts) { - $messages = array(); - $expectedParts = json_decode(urldecode($controller->request->data['_Token']['debug']), true); - if (!is_array($expectedParts) || count($expectedParts) !== 3) { - return 'Invalid security debug token.'; - } - $expectedUrl = Hash::get($expectedParts, 0); - $url = Hash::get($hashParts, 0); - if ($expectedUrl !== $url) { - $messages[] = sprintf('URL mismatch in POST data (expected \'%s\' but found \'%s\')', $expectedUrl, $url); - } - $expectedFields = Hash::get($expectedParts, 1); - $dataFields = Hash::get($hashParts, 1); - if ($dataFields) { - $dataFields = unserialize($dataFields); - } - $fieldsMessages = $this->_debugCheckFields( - $dataFields, - $expectedFields, - 'Unexpected field \'%s\' in POST data', - 'Tampered field \'%s\' in POST data (expected value \'%s\' but found \'%s\')', - 'Missing field \'%s\' in POST data' - ); - $expectedUnlockedFields = Hash::get($expectedParts, 2); - $dataUnlockedFields = Hash::get($hashParts, 2) ?: array(); - if ($dataUnlockedFields) { - $dataUnlockedFields = explode('|', $dataUnlockedFields); - } - $unlockFieldsMessages = $this->_debugCheckFields( - $dataUnlockedFields, - $expectedUnlockedFields, - 'Unexpected unlocked field \'%s\' in POST data', - null, - 'Missing unlocked field: \'%s\'' - ); - - $messages = array_merge($messages, $fieldsMessages, $unlockFieldsMessages); - - return implode(', ', $messages); - } - -/** - * Iterates data array to check against expected - * - * @param array $dataFields Fields array, containing the POST data fields - * @param array $expectedFields Fields array, containing the expected fields we should have in POST - * @param string $intKeyMessage Message string if unexpected found in data fields indexed by int (not protected) - * @param string $stringKeyMessage Message string if tampered found in data fields indexed by string (protected) - * @param string $missingMessage Message string if missing field - * @return array Messages - */ - protected function _debugCheckFields($dataFields, $expectedFields = array(), $intKeyMessage = '', $stringKeyMessage = '', $missingMessage = '') { - $messages = $this->_matchExistingFields($dataFields, $expectedFields, $intKeyMessage, $stringKeyMessage); - $expectedFieldsMessage = $this->_debugExpectedFields($expectedFields, $missingMessage); - if ($expectedFieldsMessage !== null) { - $messages[] = $expectedFieldsMessage; - } - - return $messages; - } - -/** - * Manually add CSRF token information into the provided request object. - * - * @param CakeRequest $request The request object to add into. - * @return bool - */ - public function generateToken(CakeRequest $request) { - if (isset($request->params['requested']) && $request->params['requested'] === 1) { - if ($this->Session->check('_Token')) { - $request->params['_Token'] = $this->Session->read('_Token'); - } - return false; - } - $authKey = hash('sha512', Security::randomBytes(16), false); - $token = array( - 'key' => $authKey, - 'allowedControllers' => $this->allowedControllers, - 'allowedActions' => $this->allowedActions, - 'unlockedFields' => array_merge($this->disabledFields, $this->unlockedFields), - 'csrfTokens' => array() - ); - - $tokenData = array(); - if ($this->Session->check('_Token')) { - $tokenData = $this->Session->read('_Token'); - if (!empty($tokenData['csrfTokens']) && is_array($tokenData['csrfTokens'])) { - $token['csrfTokens'] = $this->_expireTokens($tokenData['csrfTokens']); - } - } - if ($this->csrfUseOnce || empty($token['csrfTokens'])) { - $token['csrfTokens'][$authKey] = strtotime($this->csrfExpires); - } - if (!$this->csrfUseOnce) { - $csrfTokens = array_keys($token['csrfTokens']); - $authKey = $csrfTokens[0]; - $token['key'] = $authKey; - $token['csrfTokens'][$authKey] = strtotime($this->csrfExpires); - } - $this->Session->write('_Token', $token); - $request->params['_Token'] = array( - 'key' => $token['key'], - 'unlockedFields' => $token['unlockedFields'] - ); - return true; - } - -/** - * Validate that the controller has a CSRF token in the POST data - * and that the token is legit/not expired. If the token is valid - * it will be removed from the list of valid tokens. - * - * @param Controller $controller A controller to check - * @throws SecurityException - * @return bool Valid csrf token. - */ - protected function _validateCsrf(Controller $controller) { - $token = $this->Session->read('_Token'); - $requestToken = $controller->request->data('_Token.key'); - - if (!$requestToken) { - throw new SecurityException('Missing CSRF token'); - } - - if (!isset($token['csrfTokens'][$requestToken])) { - throw new SecurityException('CSRF token mismatch'); - } - - if ($token['csrfTokens'][$requestToken] < time()) { - throw new SecurityException('CSRF token expired'); - } - - if ($this->csrfUseOnce) { - $this->Session->delete('_Token.csrfTokens.' . $requestToken); - } - return true; - } - -/** - * Expire CSRF nonces and remove them from the valid tokens. - * Uses a simple timeout to expire the tokens. - * - * @param array $tokens An array of nonce => expires. - * @return array An array of nonce => expires. - */ - protected function _expireTokens($tokens) { - $now = time(); - foreach ($tokens as $nonce => $expires) { - if ($expires < $now) { - unset($tokens[$nonce]); - } - } - $overflow = count($tokens) - $this->csrfLimit; - if ($overflow > 0) { - $tokens = array_slice($tokens, $overflow + 1, null, true); - } - return $tokens; - } - -/** - * Calls a controller callback method - * - * @param Controller $controller Controller to run callback on - * @param string $method Method to execute - * @param array $params Parameters to send to method - * @return mixed Controller callback method's response - * @throws BadRequestException When a the blackholeCallback is not callable. - */ - protected function _callback(Controller $controller, $method, $params = array()) { - if (!is_callable(array($controller, $method))) { - throw new BadRequestException(__d('cake_dev', 'The request has been black-holed')); - } - return call_user_func_array(array(&$controller, $method), empty($params) ? null : $params); - } - -/** - * Generate array of messages for the existing fields in POST data, matching dataFields in $expectedFields - * will be unset - * - * @param array $dataFields Fields array, containing the POST data fields - * @param array &$expectedFields Fields array, containing the expected fields we should have in POST - * @param string $intKeyMessage Message string if unexpected found in data fields indexed by int (not protected) - * @param string $stringKeyMessage Message string if tampered found in data fields indexed by string (protected) - * @return array Error messages - */ - protected function _matchExistingFields($dataFields, &$expectedFields, $intKeyMessage, $stringKeyMessage) { - $messages = array(); - foreach ((array)$dataFields as $key => $value) { - if (is_int($key)) { - $foundKey = array_search($value, (array)$expectedFields); - if ($foundKey === false) { - $messages[] = sprintf($intKeyMessage, $value); - } else { - unset($expectedFields[$foundKey]); - } - } elseif (is_string($key)) { - if (isset($expectedFields[$key]) && $value !== $expectedFields[$key]) { - $messages[] = sprintf($stringKeyMessage, $key, $expectedFields[$key], $value); - } - unset($expectedFields[$key]); - } - } - - return $messages; - } - -/** - * Generate debug message for the expected fields - * - * @param array $expectedFields Expected fields - * @param string $missingMessage Message template - * @return string Error message about expected fields - */ - protected function _debugExpectedFields($expectedFields = array(), $missingMessage = '') { - if (count($expectedFields) === 0) { - return null; - } - - $expectedFieldNames = array(); - foreach ((array)$expectedFields as $key => $expectedField) { - if (is_int($key)) { - $expectedFieldNames[] = $expectedField; - } else { - $expectedFieldNames[] = $key; - } - } - - return sprintf($missingMessage, implode(', ', $expectedFieldNames)); - } +class SecurityComponent extends Component +{ + + /** + * Default message used for exceptions thrown + */ + const DEFAULT_EXCEPTION_MESSAGE = 'The request has been black-holed'; + + /** + * The controller method that will be called if this request is black-hole'd + * + * @var string + */ + public $blackHoleCallback = null; + + /** + * List of controller actions for which a POST request is required + * + * @var array + * @deprecated 3.0.0 Use CakeRequest::allowMethod() instead. + * @see SecurityComponent::requirePost() + */ + public $requirePost = []; + + /** + * List of controller actions for which a GET request is required + * + * @var array + * @deprecated 3.0.0 Use CakeRequest::allowMethod() instead. + * @see SecurityComponent::requireGet() + */ + public $requireGet = []; + + /** + * List of controller actions for which a PUT request is required + * + * @var array + * @deprecated 3.0.0 Use CakeRequest::allowMethod() instead. + * @see SecurityComponent::requirePut() + */ + public $requirePut = []; + + /** + * List of controller actions for which a DELETE request is required + * + * @var array + * @deprecated 3.0.0 Use CakeRequest::allowMethod() instead. + * @see SecurityComponent::requireDelete() + */ + public $requireDelete = []; + + /** + * List of actions that require an SSL-secured connection + * + * @var array + * @see SecurityComponent::requireSecure() + */ + public $requireSecure = []; + + /** + * List of actions that require a valid authentication key + * + * @var array + * @see SecurityComponent::requireAuth() + * @deprecated 2.8.1 This feature is confusing and not useful. + */ + public $requireAuth = []; + + /** + * Controllers from which actions of the current controller are allowed to receive + * requests. + * + * @var array + * @see SecurityComponent::requireAuth() + */ + public $allowedControllers = []; + + /** + * Actions from which actions of the current controller are allowed to receive + * requests. + * + * @var array + * @see SecurityComponent::requireAuth() + */ + public $allowedActions = []; + + /** + * Deprecated property, superseded by unlockedFields. + * + * @var array + * @deprecated 3.0.0 Superseded by unlockedFields. + * @see SecurityComponent::$unlockedFields + */ + public $disabledFields = []; + + /** + * Form fields to exclude from POST validation. Fields can be unlocked + * either in the Component, or with FormHelper::unlockField(). + * Fields that have been unlocked are not required to be part of the POST + * and hidden unlocked fields do not have their values checked. + * + * @var array + */ + public $unlockedFields = []; + + /** + * Actions to exclude from CSRF and POST validation checks. + * Other checks like requireAuth(), requireSecure(), + * requirePost(), requireGet() etc. will still be applied. + * + * @var array + */ + public $unlockedActions = []; + + /** + * Whether to validate POST data. Set to false to disable for data coming from 3rd party + * services, etc. + * + * @var bool + */ + public $validatePost = true; + + /** + * Whether to use CSRF protected forms. Set to false to disable CSRF protection on forms. + * + * @var bool + * @see http://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF) + * @see SecurityComponent::$csrfExpires + */ + public $csrfCheck = true; + + /** + * The duration from when a CSRF token is created that it will expire on. + * Each form/page request will generate a new token that can only be submitted once unless + * it expires. Can be any value compatible with strtotime() + * + * @var string + */ + public $csrfExpires = '+30 minutes'; + + /** + * Controls whether or not CSRF tokens are use and burn. Set to false to not generate + * new tokens on each request. One token will be reused until it expires. This reduces + * the chances of users getting invalid requests because of token consumption. + * It has the side effect of making CSRF less secure, as tokens are reusable. + * + * @var bool + */ + public $csrfUseOnce = true; + + /** + * Control the number of tokens a user can keep open. + * This is most useful with one-time use tokens. Since new tokens + * are created on each request, having a hard limit on the number of open tokens + * can be useful in controlling the size of the session file. + * + * When tokens are evicted, the oldest ones will be removed, as they are the most likely + * to be dead/expired. + * + * @var int + */ + public $csrfLimit = 100; + + /** + * Other components used by the Security component + * + * @var array + */ + public $components = ['Session']; + /** + * Request object + * + * @var CakeRequest + */ + public $request; + /** + * Holds the current action of the controller + * + * @var string + */ + protected $_action = null; + + /** + * Component startup. All security checking happens here. + * + * @param Controller $controller Instantiating controller + * @return void + * @throws AuthSecurityException + */ + public function startup(Controller $controller) + { + $this->request = $controller->request; + $this->_action = $controller->request->params['action']; + $hasData = ($controller->request->data || $controller->request->is(['put', 'post', 'delete', 'patch'])); + try { + $this->_methodsRequired($controller); + $this->_secureRequired($controller); + $this->_authRequired($controller); + + $isNotRequestAction = ( + !isset($controller->request->params['requested']) || + $controller->request->params['requested'] != 1 + ); + + if ($this->_action === $this->blackHoleCallback) { + throw new AuthSecurityException(sprintf('Action %s is defined as the blackhole callback.', $this->_action)); + } + + if (!in_array($this->_action, (array)$this->unlockedActions) && $hasData && $isNotRequestAction) { + if ($this->validatePost) { + $this->_validatePost($controller); + } + if ($this->csrfCheck) { + $this->_validateCsrf($controller); + } + } + + } catch (SecurityException $se) { + return $this->blackHole($controller, $se->getType(), $se); + } + + $this->generateToken($controller->request); + if ($hasData && is_array($controller->request->data)) { + unset($controller->request->data['_Token']); + } + } + + /** + * Check if HTTP methods are required + * + * @param Controller $controller Instantiating controller + * @return bool True if $method is required + * @throws SecurityException + */ + protected function _methodsRequired(Controller $controller) + { + foreach (['Post', 'Get', 'Put', 'Delete'] as $method) { + $property = 'require' . $method; + if (is_array($this->$property) && !empty($this->$property)) { + $require = $this->$property; + if (in_array($this->_action, $require) || $this->$property === ['*']) { + if (!$controller->request->is($method)) { + throw new SecurityException( + sprintf('The request method must be %s', strtoupper($method)) + ); + } + } + } + } + return true; + } + + /** + * Check if access requires secure connection + * + * @param Controller $controller Instantiating controller + * @return bool True if secure connection required + * @throws SecurityException + */ + protected function _secureRequired(Controller $controller) + { + if (is_array($this->requireSecure) && !empty($this->requireSecure)) { + $requireSecure = $this->requireSecure; + + if (in_array($this->_action, $requireSecure) || $this->requireSecure === ['*']) { + if (!$controller->request->is('ssl')) { + throw new SecurityException( + 'Request is not SSL and the action is required to be secure' + ); + } + } + } + return true; + } + + /** + * Check if authentication is required + * + * @param Controller $controller Instantiating controller + * @return bool|null True if authentication required + * @throws AuthSecurityException + * @deprecated 2.8.1 This feature is confusing and not useful. + */ + protected function _authRequired(Controller $controller) + { + if (is_array($this->requireAuth) && !empty($this->requireAuth) && !empty($controller->request->data)) { + $requireAuth = $this->requireAuth; + + if (in_array($controller->request->params['action'], $requireAuth) || $this->requireAuth === ['*']) { + if (!isset($controller->request->data['_Token'])) { + throw new AuthSecurityException('\'_Token\' was not found in request data.'); + } + + if ($this->Session->check('_Token')) { + $tData = $this->Session->read('_Token'); + + if (!empty($tData['allowedControllers']) && + !in_array($controller->request->params['controller'], $tData['allowedControllers'])) { + throw new AuthSecurityException( + sprintf( + 'Controller \'%s\' was not found in allowed controllers: \'%s\'.', + $controller->request->params['controller'], + implode(', ', (array)$tData['allowedControllers']) + ) + ); + } + if (!empty($tData['allowedActions']) && + !in_array($controller->request->params['action'], $tData['allowedActions']) + ) { + throw new AuthSecurityException( + sprintf( + 'Action \'%s::%s\' was not found in allowed actions: \'%s\'.', + $controller->request->params['controller'], + $controller->request->params['action'], + implode(', ', (array)$tData['allowedActions']) + ) + ); + } + } else { + throw new AuthSecurityException('\'_Token\' was not found in session.'); + } + } + } + return true; + } + + /** + * Validate submitted form + * + * @param Controller $controller Instantiating controller + * @return bool true if submitted form is valid + * @throws AuthSecurityException + */ + protected function _validatePost(Controller $controller) + { + $token = $this->_validToken($controller); + $hashParts = $this->_hashParts($controller); + $check = Security::hash(implode('', $hashParts), 'sha1'); + + if ($token === $check) { + return true; + } + + $msg = self::DEFAULT_EXCEPTION_MESSAGE; + if (Configure::read('debug')) { + $msg = $this->_debugPostTokenNotMatching($controller, $hashParts); + } + + throw new AuthSecurityException($msg); + } + + /** + * Check if token is valid + * + * @param Controller $controller Instantiating controller + * @return string fields token + * @throws SecurityException + * @throws AuthSecurityException + */ + protected function _validToken(Controller $controller) + { + $check = $controller->request->data; + + $message = '\'%s\' was not found in request data.'; + if (!isset($check['_Token'])) { + throw new AuthSecurityException(sprintf($message, '_Token')); + } + if (!isset($check['_Token']['fields'])) { + throw new AuthSecurityException(sprintf($message, '_Token.fields')); + } + if (!isset($check['_Token']['unlocked'])) { + throw new AuthSecurityException(sprintf($message, '_Token.unlocked')); + } + if (Configure::read('debug') && !isset($check['_Token']['debug'])) { + throw new SecurityException(sprintf($message, '_Token.debug')); + } + if (!Configure::read('debug') && isset($check['_Token']['debug'])) { + throw new SecurityException('Unexpected \'_Token.debug\' found in request data'); + } + + $token = urldecode($check['_Token']['fields']); + if (strpos($token, ':')) { + list($token,) = explode(':', $token, 2); + } + + return $token; + } + + /** + * Return hash parts for the Token generation + * + * @param Controller $controller Instantiating controller + * @return array + */ + protected function _hashParts(Controller $controller) + { + $fieldList = $this->_fieldsList($controller->request->data); + $unlocked = $this->_sortedUnlocked($controller->request->data); + + return [ + $controller->request->here(), + serialize($fieldList), + $unlocked, + Configure::read('Security.salt') + ]; + } + + /** + * Return the fields list for the hash calculation + * + * @param array $check Data array + * @return array + */ + protected function _fieldsList(array $check) + { + $locked = ''; + $token = urldecode($check['_Token']['fields']); + $unlocked = $this->_unlocked($check); + + if (strpos($token, ':')) { + list($token, $locked) = explode(':', $token, 2); + } + unset($check['_Token'], $check['_csrfToken']); + + $locked = explode('|', $locked); + $unlocked = explode('|', $unlocked); + + $fields = Hash::flatten($check); + $fieldList = array_keys($fields); + $multi = $lockedFields = []; + $isUnlocked = false; + + foreach ($fieldList as $i => $key) { + if (preg_match('/(\.\d+){1,10}$/', $key)) { + $multi[$i] = preg_replace('/(\.\d+){1,10}$/', '', $key); + unset($fieldList[$i]); + } else { + $fieldList[$i] = (string)$key; + } + } + if (!empty($multi)) { + $fieldList += array_unique($multi); + } + + $unlockedFields = array_unique( + array_merge((array)$this->disabledFields, (array)$this->unlockedFields, $unlocked) + ); + + foreach ($fieldList as $i => $key) { + $isLocked = (is_array($locked) && in_array($key, $locked)); + + if (!empty($unlockedFields)) { + foreach ($unlockedFields as $off) { + $off = explode('.', $off); + $field = array_values(array_intersect(explode('.', $key), $off)); + $isUnlocked = ($field === $off); + if ($isUnlocked) { + break; + } + } + } + + if ($isUnlocked || $isLocked) { + unset($fieldList[$i]); + if ($isLocked) { + $lockedFields[$key] = $fields[$key]; + } + } + } + sort($fieldList, SORT_STRING); + ksort($lockedFields, SORT_STRING); + $fieldList += $lockedFields; + + return $fieldList; + } + + /** + * Get the unlocked string + * + * @param array $data Data array + * @return string + */ + protected function _unlocked(array $data) + { + return urldecode($data['_Token']['unlocked']); + } + + /** + * Get the sorted unlocked string + * + * @param array $data Data array + * @return string + */ + protected function _sortedUnlocked($data) + { + $unlocked = $this->_unlocked($data); + $unlocked = explode('|', $unlocked); + sort($unlocked, SORT_STRING); + + return implode('|', $unlocked); + } + + /** + * Create a message for humans to understand why Security token is not matching + * + * @param Controller $controller Instantiating controller + * @param array $hashParts Elements used to generate the Token hash + * @return string Message explaining why the tokens are not matching + */ + protected function _debugPostTokenNotMatching(Controller $controller, $hashParts) + { + $messages = []; + $expectedParts = json_decode(urldecode($controller->request->data['_Token']['debug']), true); + if (!is_array($expectedParts) || count($expectedParts) !== 3) { + return 'Invalid security debug token.'; + } + $expectedUrl = Hash::get($expectedParts, 0); + $url = Hash::get($hashParts, 0); + if ($expectedUrl !== $url) { + $messages[] = sprintf('URL mismatch in POST data (expected \'%s\' but found \'%s\')', $expectedUrl, $url); + } + $expectedFields = Hash::get($expectedParts, 1); + $dataFields = Hash::get($hashParts, 1); + if ($dataFields) { + $dataFields = unserialize($dataFields); + } + $fieldsMessages = $this->_debugCheckFields( + $dataFields, + $expectedFields, + 'Unexpected field \'%s\' in POST data', + 'Tampered field \'%s\' in POST data (expected value \'%s\' but found \'%s\')', + 'Missing field \'%s\' in POST data' + ); + $expectedUnlockedFields = Hash::get($expectedParts, 2); + $dataUnlockedFields = Hash::get($hashParts, 2) ?: []; + if ($dataUnlockedFields) { + $dataUnlockedFields = explode('|', $dataUnlockedFields); + } + $unlockFieldsMessages = $this->_debugCheckFields( + $dataUnlockedFields, + $expectedUnlockedFields, + 'Unexpected unlocked field \'%s\' in POST data', + null, + 'Missing unlocked field: \'%s\'' + ); + + $messages = array_merge($messages, $fieldsMessages, $unlockFieldsMessages); + + return implode(', ', $messages); + } + + /** + * Iterates data array to check against expected + * + * @param array $dataFields Fields array, containing the POST data fields + * @param array $expectedFields Fields array, containing the expected fields we should have in POST + * @param string $intKeyMessage Message string if unexpected found in data fields indexed by int (not protected) + * @param string $stringKeyMessage Message string if tampered found in data fields indexed by string (protected) + * @param string $missingMessage Message string if missing field + * @return array Messages + */ + protected function _debugCheckFields($dataFields, $expectedFields = [], $intKeyMessage = '', $stringKeyMessage = '', $missingMessage = '') + { + $messages = $this->_matchExistingFields($dataFields, $expectedFields, $intKeyMessage, $stringKeyMessage); + $expectedFieldsMessage = $this->_debugExpectedFields($expectedFields, $missingMessage); + if ($expectedFieldsMessage !== null) { + $messages[] = $expectedFieldsMessage; + } + + return $messages; + } + + /** + * Generate array of messages for the existing fields in POST data, matching dataFields in $expectedFields + * will be unset + * + * @param array $dataFields Fields array, containing the POST data fields + * @param array &$expectedFields Fields array, containing the expected fields we should have in POST + * @param string $intKeyMessage Message string if unexpected found in data fields indexed by int (not protected) + * @param string $stringKeyMessage Message string if tampered found in data fields indexed by string (protected) + * @return array Error messages + */ + protected function _matchExistingFields($dataFields, &$expectedFields, $intKeyMessage, $stringKeyMessage) + { + $messages = []; + foreach ((array)$dataFields as $key => $value) { + if (is_int($key)) { + $foundKey = array_search($value, (array)$expectedFields); + if ($foundKey === false) { + $messages[] = sprintf($intKeyMessage, $value); + } else { + unset($expectedFields[$foundKey]); + } + } else if (is_string($key)) { + if (isset($expectedFields[$key]) && $value !== $expectedFields[$key]) { + $messages[] = sprintf($stringKeyMessage, $key, $expectedFields[$key], $value); + } + unset($expectedFields[$key]); + } + } + + return $messages; + } + + /** + * Generate debug message for the expected fields + * + * @param array $expectedFields Expected fields + * @param string $missingMessage Message template + * @return string Error message about expected fields + */ + protected function _debugExpectedFields($expectedFields = [], $missingMessage = '') + { + if (count($expectedFields) === 0) { + return null; + } + + $expectedFieldNames = []; + foreach ((array)$expectedFields as $key => $expectedField) { + if (is_int($key)) { + $expectedFieldNames[] = $expectedField; + } else { + $expectedFieldNames[] = $key; + } + } + + return sprintf($missingMessage, implode(', ', $expectedFieldNames)); + } + + /** + * Validate that the controller has a CSRF token in the POST data + * and that the token is legit/not expired. If the token is valid + * it will be removed from the list of valid tokens. + * + * @param Controller $controller A controller to check + * @return bool Valid csrf token. + * @throws SecurityException + */ + protected function _validateCsrf(Controller $controller) + { + $token = $this->Session->read('_Token'); + $requestToken = $controller->request->data('_Token.key'); + + if (!$requestToken) { + throw new SecurityException('Missing CSRF token'); + } + + if (!isset($token['csrfTokens'][$requestToken])) { + throw new SecurityException('CSRF token mismatch'); + } + + if ($token['csrfTokens'][$requestToken] < time()) { + throw new SecurityException('CSRF token expired'); + } + + if ($this->csrfUseOnce) { + $this->Session->delete('_Token.csrfTokens.' . $requestToken); + } + return true; + } + + /** + * Black-hole an invalid request with a 400 error or custom callback. If SecurityComponent::$blackHoleCallback + * is specified, it will use this callback by executing the method indicated in $error + * + * @param Controller $controller Instantiating controller + * @param string $error Error method + * @param SecurityException|null $exception Additional debug info describing the cause + * @return mixed If specified, controller blackHoleCallback's response, or no return otherwise + * @throws BadRequestException + * @link https://book.cakephp.org/2.0/en/core-libraries/components/security-component.html#handling-blackhole-callbacks + * @see SecurityComponent::$blackHoleCallback + */ + public function blackHole(Controller $controller, $error = '', SecurityException $exception = null) + { + if (!$this->blackHoleCallback) { + $this->_throwException($exception); + } + return $this->_callback($controller, $this->blackHoleCallback, [$error]); + } + + /** + * Check debug status and throw an Exception based on the existing one + * + * @param SecurityException|null $exception Additional debug info describing the cause + * @return void + * @throws BadRequestException + */ + protected function _throwException($exception = null) + { + if ($exception !== null) { + if (!Configure::read('debug') && $exception instanceof SecurityException) { + $exception->setReason($exception->getMessage()); + $exception->setMessage(self::DEFAULT_EXCEPTION_MESSAGE); + } + throw $exception; + } + throw new BadRequestException(self::DEFAULT_EXCEPTION_MESSAGE); + } + + /** + * Calls a controller callback method + * + * @param Controller $controller Controller to run callback on + * @param string $method Method to execute + * @param array $params Parameters to send to method + * @return mixed Controller callback method's response + * @throws BadRequestException When a the blackholeCallback is not callable. + */ + protected function _callback(Controller $controller, $method, $params = []) + { + if (!is_callable([$controller, $method])) { + throw new BadRequestException(__d('cake_dev', 'The request has been black-holed')); + } + return call_user_func_array([&$controller, $method], empty($params) ? null : $params); + } + + /** + * Manually add CSRF token information into the provided request object. + * + * @param CakeRequest $request The request object to add into. + * @return bool + */ + public function generateToken(CakeRequest $request) + { + if (isset($request->params['requested']) && $request->params['requested'] === 1) { + if ($this->Session->check('_Token')) { + $request->params['_Token'] = $this->Session->read('_Token'); + } + return false; + } + $authKey = hash('sha512', Security::randomBytes(16), false); + $token = [ + 'key' => $authKey, + 'allowedControllers' => $this->allowedControllers, + 'allowedActions' => $this->allowedActions, + 'unlockedFields' => array_merge($this->disabledFields, $this->unlockedFields), + 'csrfTokens' => [] + ]; + + $tokenData = []; + if ($this->Session->check('_Token')) { + $tokenData = $this->Session->read('_Token'); + if (!empty($tokenData['csrfTokens']) && is_array($tokenData['csrfTokens'])) { + $token['csrfTokens'] = $this->_expireTokens($tokenData['csrfTokens']); + } + } + if ($this->csrfUseOnce || empty($token['csrfTokens'])) { + $token['csrfTokens'][$authKey] = strtotime($this->csrfExpires); + } + if (!$this->csrfUseOnce) { + $csrfTokens = array_keys($token['csrfTokens']); + $authKey = $csrfTokens[0]; + $token['key'] = $authKey; + $token['csrfTokens'][$authKey] = strtotime($this->csrfExpires); + } + $this->Session->write('_Token', $token); + $request->params['_Token'] = [ + 'key' => $token['key'], + 'unlockedFields' => $token['unlockedFields'] + ]; + return true; + } + + /** + * Expire CSRF nonces and remove them from the valid tokens. + * Uses a simple timeout to expire the tokens. + * + * @param array $tokens An array of nonce => expires. + * @return array An array of nonce => expires. + */ + protected function _expireTokens($tokens) + { + $now = time(); + foreach ($tokens as $nonce => $expires) { + if ($expires < $now) { + unset($tokens[$nonce]); + } + } + $overflow = count($tokens) - $this->csrfLimit; + if ($overflow > 0) { + $tokens = array_slice($tokens, $overflow + 1, null, true); + } + return $tokens; + } + + /** + * Sets the actions that require a POST request, or empty for all actions + * + * @return void + * @deprecated 3.0.0 Use CakeRequest::onlyAllow() instead. + * @link https://book.cakephp.org/2.0/en/core-libraries/components/security-component.html#SecurityComponent::requirePost + */ + public function requirePost() + { + $args = func_get_args(); + $this->_requireMethod('Post', $args); + } + + /** + * Sets the actions that require a $method HTTP request, or empty for all actions + * + * @param string $method The HTTP method to assign controller actions to + * @param array $actions Controller actions to set the required HTTP method to. + * @return void + */ + protected function _requireMethod($method, $actions = []) + { + if (isset($actions[0]) && is_array($actions[0])) { + $actions = $actions[0]; + } + $this->{'require' . $method} = (empty($actions)) ? ['*'] : $actions; + } + + /** + * Sets the actions that require a GET request, or empty for all actions + * + * @return void + * @deprecated 3.0.0 Use CakeRequest::onlyAllow() instead. + */ + public function requireGet() + { + $args = func_get_args(); + $this->_requireMethod('Get', $args); + } + + /** + * Sets the actions that require a PUT request, or empty for all actions + * + * @return void + * @deprecated 3.0.0 Use CakeRequest::onlyAllow() instead. + */ + public function requirePut() + { + $args = func_get_args(); + $this->_requireMethod('Put', $args); + } + + /** + * Sets the actions that require a DELETE request, or empty for all actions + * + * @return void + * @deprecated 3.0.0 Use CakeRequest::onlyAllow() instead. + */ + public function requireDelete() + { + $args = func_get_args(); + $this->_requireMethod('Delete', $args); + } + + /** + * Sets the actions that require a request that is SSL-secured, or empty for all actions + * + * @return void + * @link https://book.cakephp.org/2.0/en/core-libraries/components/security-component.html#SecurityComponent::requireSecure + */ + public function requireSecure() + { + $args = func_get_args(); + $this->_requireMethod('Secure', $args); + } + + /** + * Sets the actions that require whitelisted form submissions. + * + * Adding actions with this method will enforce the restrictions + * set in SecurityComponent::$allowedControllers and + * SecurityComponent::$allowedActions. + * + * @return void + * @link https://book.cakephp.org/2.0/en/core-libraries/components/security-component.html#SecurityComponent::requireAuth + */ + public function requireAuth() + { + $args = func_get_args(); + $this->_requireMethod('Auth', $args); + } } diff --git a/lib/Cake/Controller/Component/SessionComponent.php b/lib/Cake/Controller/Component/SessionComponent.php index f0955095..b3ff2d25 100755 --- a/lib/Cake/Controller/Component/SessionComponent.php +++ b/lib/Cake/Controller/Component/SessionComponent.php @@ -28,181 +28,195 @@ * @link https://book.cakephp.org/2.0/en/core-libraries/components/sessions.html * @link https://book.cakephp.org/2.0/en/development/sessions.html */ -class SessionComponent extends Component { - -/** - * Get / Set the userAgent - * - * @param string $userAgent Set the userAgent - * @return string Current user agent. - */ - public function userAgent($userAgent = null) { - return CakeSession::userAgent($userAgent); - } - -/** - * Writes a value to a session key. - * - * In your controller: $this->Session->write('Controller.sessKey', 'session value'); - * - * @param string|array $name The name of the key your are setting in the session. - * This should be in a Controller.key format for better organizing - * @param mixed $value The value you want to store in a session. - * @return bool Success - * @link https://book.cakephp.org/2.0/en/core-libraries/components/sessions.html#SessionComponent::write - */ - public function write($name, $value = null) { - return CakeSession::write($name, $value); - } - -/** - * Reads a session value for a key or returns values for all keys. - * - * In your controller: $this->Session->read('Controller.sessKey'); - * Calling the method without a param will return all session vars - * - * @param string $name the name of the session key you want to read - * @return mixed value from the session vars - * @link https://book.cakephp.org/2.0/en/core-libraries/components/sessions.html#SessionComponent::read - */ - public function read($name = null) { - return CakeSession::read($name); - } - -/** - * Deletes a session value for a key. - * - * In your controller: $this->Session->delete('Controller.sessKey'); - * - * @param string $name the name of the session key you want to delete - * @return bool true is session variable is set and can be deleted, false is variable was not set. - * @link https://book.cakephp.org/2.0/en/core-libraries/components/sessions.html#SessionComponent::delete - */ - public function delete($name) { - return CakeSession::delete($name); - } - -/** - * Reads and deletes a session value for a key. - * - * In your controller: `$this->Session->consume('Controller.sessKey');` - * - * @param string $name the name of the session key you want to read - * @return mixed values from the session vars - */ - public function consume($name) { - return CakeSession::consume($name); - } - -/** - * Checks if a session variable is set. - * - * In your controller: $this->Session->check('Controller.sessKey'); - * - * @param string $name the name of the session key you want to check - * @return bool true is session variable is set, false if not - * @link https://book.cakephp.org/2.0/en/core-libraries/components/sessions.html#SessionComponent::check - */ - public function check($name) { - return CakeSession::check($name); - } - -/** - * Used to determine the last error in a session. - * - * In your controller: $this->Session->error(); - * - * @return string Last session error - */ - public function error() { - return CakeSession::error(); - } - -/** - * Used to set a session variable that can be used to output messages in the view. - * - * In your controller: $this->Session->setFlash('This has been saved'); - * - * Additional params below can be passed to customize the output, or the Message.[key]. - * You can also set additional parameters when rendering flash messages. See SessionHelper::flash() - * for more information on how to do that. - * - * @param string $message Message to be flashed - * @param string $element Element to wrap flash message in. - * @param array $params Parameters to be sent to layout as view variables - * @param string $key Message key, default is 'flash' - * @return void - * @link https://book.cakephp.org/2.0/en/core-libraries/components/sessions.html#creating-notification-messages - * @deprecated 3.0.0 Since 2.7, use the FlashComponent instead. - */ - public function setFlash($message, $element = 'default', $params = array(), $key = 'flash') { - $messages = (array)CakeSession::read('Message.' . $key); - $messages[] = array( - 'message' => $message, - 'element' => $element, - 'params' => $params, - ); - CakeSession::write('Message.' . $key, $messages); - } - -/** - * Used to renew a session id - * - * In your controller: $this->Session->renew(); - * - * @return void - */ - public function renew() { - CakeSession::renew(); - } - -/** - * Used to check for a valid session. - * - * In your controller: $this->Session->valid(); - * - * @return bool true is session is valid, false is session is invalid - */ - public function valid() { - return CakeSession::valid(); - } - -/** - * Used to destroy sessions - * - * In your controller: $this->Session->destroy(); - * - * @return void - * @link https://book.cakephp.org/2.0/en/core-libraries/components/sessions.html#SessionComponent::destroy - */ - public function destroy() { - CakeSession::destroy(); - } - -/** - * Get/Set the session id. - * - * When fetching the session id, the session will be started - * if it has not already been started. When setting the session id, - * the session will not be started. - * - * @param string $id Id to use (optional) - * @return string The current session id. - */ - public function id($id = null) { - if (empty($id)) { - CakeSession::start(); - } - return CakeSession::id($id); - } - -/** - * Returns a bool, whether or not the session has been started. - * - * @return bool - */ - public function started() { - return CakeSession::started(); - } +class SessionComponent extends Component +{ + + /** + * Get / Set the userAgent + * + * @param string $userAgent Set the userAgent + * @return string Current user agent. + */ + public function userAgent($userAgent = null) + { + return CakeSession::userAgent($userAgent); + } + + /** + * Writes a value to a session key. + * + * In your controller: $this->Session->write('Controller.sessKey', 'session value'); + * + * @param string|array $name The name of the key your are setting in the session. + * This should be in a Controller.key format for better organizing + * @param mixed $value The value you want to store in a session. + * @return bool Success + * @link https://book.cakephp.org/2.0/en/core-libraries/components/sessions.html#SessionComponent::write + */ + public function write($name, $value = null) + { + return CakeSession::write($name, $value); + } + + /** + * Reads a session value for a key or returns values for all keys. + * + * In your controller: $this->Session->read('Controller.sessKey'); + * Calling the method without a param will return all session vars + * + * @param string $name the name of the session key you want to read + * @return mixed value from the session vars + * @link https://book.cakephp.org/2.0/en/core-libraries/components/sessions.html#SessionComponent::read + */ + public function read($name = null) + { + return CakeSession::read($name); + } + + /** + * Deletes a session value for a key. + * + * In your controller: $this->Session->delete('Controller.sessKey'); + * + * @param string $name the name of the session key you want to delete + * @return bool true is session variable is set and can be deleted, false is variable was not set. + * @link https://book.cakephp.org/2.0/en/core-libraries/components/sessions.html#SessionComponent::delete + */ + public function delete($name) + { + return CakeSession::delete($name); + } + + /** + * Reads and deletes a session value for a key. + * + * In your controller: `$this->Session->consume('Controller.sessKey');` + * + * @param string $name the name of the session key you want to read + * @return mixed values from the session vars + */ + public function consume($name) + { + return CakeSession::consume($name); + } + + /** + * Checks if a session variable is set. + * + * In your controller: $this->Session->check('Controller.sessKey'); + * + * @param string $name the name of the session key you want to check + * @return bool true is session variable is set, false if not + * @link https://book.cakephp.org/2.0/en/core-libraries/components/sessions.html#SessionComponent::check + */ + public function check($name) + { + return CakeSession::check($name); + } + + /** + * Used to determine the last error in a session. + * + * In your controller: $this->Session->error(); + * + * @return string Last session error + */ + public function error() + { + return CakeSession::error(); + } + + /** + * Used to set a session variable that can be used to output messages in the view. + * + * In your controller: $this->Session->setFlash('This has been saved'); + * + * Additional params below can be passed to customize the output, or the Message.[key]. + * You can also set additional parameters when rendering flash messages. See SessionHelper::flash() + * for more information on how to do that. + * + * @param string $message Message to be flashed + * @param string $element Element to wrap flash message in. + * @param array $params Parameters to be sent to layout as view variables + * @param string $key Message key, default is 'flash' + * @return void + * @link https://book.cakephp.org/2.0/en/core-libraries/components/sessions.html#creating-notification-messages + * @deprecated 3.0.0 Since 2.7, use the FlashComponent instead. + */ + public function setFlash($message, $element = 'default', $params = [], $key = 'flash') + { + $messages = (array)CakeSession::read('Message.' . $key); + $messages[] = [ + 'message' => $message, + 'element' => $element, + 'params' => $params, + ]; + CakeSession::write('Message.' . $key, $messages); + } + + /** + * Used to renew a session id + * + * In your controller: $this->Session->renew(); + * + * @return void + */ + public function renew() + { + CakeSession::renew(); + } + + /** + * Used to check for a valid session. + * + * In your controller: $this->Session->valid(); + * + * @return bool true is session is valid, false is session is invalid + */ + public function valid() + { + return CakeSession::valid(); + } + + /** + * Used to destroy sessions + * + * In your controller: $this->Session->destroy(); + * + * @return void + * @link https://book.cakephp.org/2.0/en/core-libraries/components/sessions.html#SessionComponent::destroy + */ + public function destroy() + { + CakeSession::destroy(); + } + + /** + * Get/Set the session id. + * + * When fetching the session id, the session will be started + * if it has not already been started. When setting the session id, + * the session will not be started. + * + * @param string $id Id to use (optional) + * @return string The current session id. + */ + public function id($id = null) + { + if (empty($id)) { + CakeSession::start(); + } + return CakeSession::id($id); + } + + /** + * Returns a bool, whether or not the session has been started. + * + * @return bool + */ + public function started() + { + return CakeSession::started(); + } } diff --git a/lib/Cake/Controller/ComponentCollection.php b/lib/Cake/Controller/ComponentCollection.php index a9500dab..e18a311f 100755 --- a/lib/Cake/Controller/ComponentCollection.php +++ b/lib/Cake/Controller/ComponentCollection.php @@ -27,114 +27,120 @@ * * @package Cake.Controller */ -class ComponentCollection extends ObjectCollection implements CakeEventListener { +class ComponentCollection extends ObjectCollection implements CakeEventListener +{ -/** - * The controller that this collection was initialized with. - * - * @var Controller - */ - protected $_Controller = null; + /** + * The controller that this collection was initialized with. + * + * @var Controller + */ + protected $_Controller = null; -/** - * Initializes all the Components for a controller. - * Attaches a reference of each component to the Controller. - * - * @param Controller $Controller Controller to initialize components for. - * @return void - */ - public function init(Controller $Controller) { - if (empty($Controller->components)) { - return; - } - $this->_Controller = $Controller; - $components = ComponentCollection::normalizeObjectArray($Controller->components); - foreach ($components as $name => $properties) { - $Controller->{$name} = $this->load($properties['class'], $properties['settings']); - } - } + /** + * Initializes all the Components for a controller. + * Attaches a reference of each component to the Controller. + * + * @param Controller $Controller Controller to initialize components for. + * @return void + */ + public function init(Controller $Controller) + { + if (empty($Controller->components)) { + return; + } + $this->_Controller = $Controller; + $components = ComponentCollection::normalizeObjectArray($Controller->components); + foreach ($components as $name => $properties) { + $Controller->{$name} = $this->load($properties['class'], $properties['settings']); + } + } -/** - * Set the controller associated with the collection. - * - * @param Controller $Controller Controller to set - * @return void - */ - public function setController(Controller $Controller) { - $this->_Controller = $Controller; - } + /** + * Loads/constructs a component. Will return the instance in the registry if it already exists. + * You can use `$settings['enabled'] = false` to disable callbacks on a component when loading it. + * Callbacks default to on. Disabled component methods work as normal, only callbacks are disabled. + * + * You can alias your component as an existing component by setting the 'className' key, i.e., + * ``` + * public $components = array( + * 'Email' => array( + * 'className' => 'AliasedEmail' + * ); + * ); + * ``` + * All calls to the `Email` component would use `AliasedEmail` instead. + * + * @param string $component Component name to load + * @param array $settings Settings for the component. + * @return Component A component object, Either the existing loaded component or a new one. + * @throws MissingComponentException when the component could not be found + */ + public function load($component, $settings = []) + { + if (isset($settings['className'])) { + $alias = $component; + $component = $settings['className']; + } + list($plugin, $name) = pluginSplit($component, true); + if (!isset($alias)) { + $alias = $name; + } + if (isset($this->_loaded[$alias])) { + return $this->_loaded[$alias]; + } + $componentClass = $name . 'Component'; + App::uses($componentClass, $plugin . 'Controller/Component'); + if (!class_exists($componentClass)) { + throw new MissingComponentException([ + 'class' => $componentClass, + 'plugin' => substr($plugin, 0, -1) + ]); + } + $this->_loaded[$alias] = new $componentClass($this, $settings); + $enable = isset($settings['enabled']) ? $settings['enabled'] : true; + if ($enable) { + $this->enable($alias); + } + return $this->_loaded[$alias]; + } -/** - * Get the controller associated with the collection. - * - * @return Controller Controller instance - */ - public function getController() { - return $this->_Controller; - } + /** + * Get the controller associated with the collection. + * + * @return Controller Controller instance + */ + public function getController() + { + return $this->_Controller; + } -/** - * Loads/constructs a component. Will return the instance in the registry if it already exists. - * You can use `$settings['enabled'] = false` to disable callbacks on a component when loading it. - * Callbacks default to on. Disabled component methods work as normal, only callbacks are disabled. - * - * You can alias your component as an existing component by setting the 'className' key, i.e., - * ``` - * public $components = array( - * 'Email' => array( - * 'className' => 'AliasedEmail' - * ); - * ); - * ``` - * All calls to the `Email` component would use `AliasedEmail` instead. - * - * @param string $component Component name to load - * @param array $settings Settings for the component. - * @return Component A component object, Either the existing loaded component or a new one. - * @throws MissingComponentException when the component could not be found - */ - public function load($component, $settings = array()) { - if (isset($settings['className'])) { - $alias = $component; - $component = $settings['className']; - } - list($plugin, $name) = pluginSplit($component, true); - if (!isset($alias)) { - $alias = $name; - } - if (isset($this->_loaded[$alias])) { - return $this->_loaded[$alias]; - } - $componentClass = $name . 'Component'; - App::uses($componentClass, $plugin . 'Controller/Component'); - if (!class_exists($componentClass)) { - throw new MissingComponentException(array( - 'class' => $componentClass, - 'plugin' => substr($plugin, 0, -1) - )); - } - $this->_loaded[$alias] = new $componentClass($this, $settings); - $enable = isset($settings['enabled']) ? $settings['enabled'] : true; - if ($enable) { - $this->enable($alias); - } - return $this->_loaded[$alias]; - } + /** + * Set the controller associated with the collection. + * + * @param Controller $Controller Controller to set + * @return void + */ + public function setController(Controller $Controller) + { + $this->_Controller = $Controller; + } -/** - * Returns the implemented events that will get routed to the trigger function - * in order to dispatch them separately on each component - * - * @return array - */ - public function implementedEvents() { - return array( - 'Controller.initialize' => array('callable' => 'trigger'), - 'Controller.startup' => array('callable' => 'trigger'), - 'Controller.beforeRender' => array('callable' => 'trigger'), - 'Controller.beforeRedirect' => array('callable' => 'trigger'), - 'Controller.shutdown' => array('callable' => 'trigger'), - ); - } + /** + * Returns the implemented events that will get routed to the trigger function + * in order to dispatch them separately on each component + * + * @return array + */ + public function implementedEvents() + { + return [ + 'Controller.initialize' => ['callable' => 'trigger'], + 'Controller.startup' => ['callable' => 'trigger'], + 'Controller.beforeRender' => ['callable' => 'trigger'], + 'Controller.beforeRedirect' => ['callable' => 'trigger'], + 'Controller.shutdown' => ['callable' => 'trigger'], + ]; + } } diff --git a/lib/Cake/Controller/Controller.php b/lib/Cake/Controller/Controller.php index 19e54692..c2586a26 100755 --- a/lib/Cake/Controller/Controller.php +++ b/lib/Cake/Controller/Controller.php @@ -62,1210 +62,1232 @@ * @property string $webroot Webroot path segment for the request. * @link https://book.cakephp.org/2.0/en/controllers.html */ -class Controller extends CakeObject implements CakeEventListener { - -/** - * The name of this controller. Controller names are plural, named after the model they manipulate. - * - * @var string - * @link https://book.cakephp.org/2.0/en/controllers.html#controller-attributes - */ - public $name = null; - -/** - * An array containing the class names of models this controller uses. - * - * Example: `public $uses = array('Product', 'Post', 'Comment');` - * - * Can be set to several values to express different options: - * - * - `true` Use the default inflected model name. - * - `array()` Use only models defined in the parent class. - * - `false` Use no models at all, do not merge with parent class either. - * - `array('Post', 'Comment')` Use only the Post and Comment models. Models - * Will also be merged with the parent class. - * - * The default value is `true`. - * - * @var bool|array - * @link https://book.cakephp.org/2.0/en/controllers.html#components-helpers-and-uses - */ - public $uses = true; - -/** - * An array containing the names of helpers this controller uses. The array elements should - * not contain the "Helper" part of the class name. - * - * Example: `public $helpers = array('Html', 'Js', 'Time', 'Ajax');` - * - * @var mixed - * @link https://book.cakephp.org/2.0/en/controllers.html#components-helpers-and-uses - */ - public $helpers = array(); - -/** - * An instance of a CakeRequest object that contains information about the current request. - * This object contains all the information about a request and several methods for reading - * additional information about the request. - * - * @var CakeRequest - * @link https://book.cakephp.org/2.0/en/controllers/request-response.html#cakerequest - */ - public $request; - -/** - * An instance of a CakeResponse object that contains information about the impending response - * - * @var CakeResponse - * @link https://book.cakephp.org/2.0/en/controllers/request-response.html#cakeresponse - */ - public $response; - -/** - * The class name to use for creating the response object. - * - * @var string - */ - protected $_responseClass = 'CakeResponse'; - -/** - * The name of the views subfolder containing views for this controller. - * - * @var string - */ - public $viewPath = null; - -/** - * The name of the layouts subfolder containing layouts for this controller. - * - * @var string - */ - public $layoutPath = null; - -/** - * Contains variables to be handed to the view. - * - * @var array - */ - public $viewVars = array(); - -/** - * The name of the view file to render. The name specified - * is the filename in /app/View/ without the .ctp extension. - * - * @var string - */ - public $view = null; - -/** - * The name of the layout file to render the view inside of. The name specified - * is the filename of the layout in /app/View/Layouts without the .ctp - * extension. If `false` then no layout is rendered. - * - * @var string|bool - */ - public $layout = 'default'; - -/** - * Set to true to automatically render the view - * after action logic. - * - * @var bool - */ - public $autoRender = true; - -/** - * Set to true to automatically render the layout around views. - * - * @var bool - */ - public $autoLayout = true; - -/** - * Instance of ComponentCollection used to handle callbacks. - * - * @var ComponentCollection - */ - public $Components = null; - -/** - * Array containing the names of components this controller uses. Component names - * should not contain the "Component" portion of the class name. - * - * Example: `public $components = array('Session', 'RequestHandler', 'Acl');` - * - * @var array - * @link https://book.cakephp.org/2.0/en/controllers/components.html - */ - public $components = array('Session', 'Flash'); - -/** - * The name of the View class this controller sends output to. - * - * @var string - */ - public $viewClass = 'View'; - -/** - * Instance of the View created during rendering. Won't be set until after - * Controller::render() is called. - * - * @var View - */ - public $View; - -/** - * File extension for view templates. Defaults to CakePHP's conventional ".ctp". - * - * @var string - */ - public $ext = '.ctp'; - -/** - * Automatically set to the name of a plugin. - * - * @var string - */ - public $plugin = null; - -/** - * Used to define methods a controller that will be cached. To cache a - * single action, the value is set to an array containing keys that match - * action names and values that denote cache expiration times (in seconds). - * - * Example: - * - * ``` - * public $cacheAction = array( - * 'view/23/' => 21600, - * 'recalled/' => 86400 - * ); - * ``` - * - * $cacheAction can also be set to a strtotime() compatible string. This - * marks all the actions in the controller for view caching. - * - * @var mixed - * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/cache.html#additional-configuration-options - */ - public $cacheAction = false; - -/** - * Holds all params passed and named. - * - * @var mixed - */ - public $passedArgs = array(); - -/** - * Triggers Scaffolding - * - * @var mixed - * @link https://book.cakephp.org/2.0/en/controllers/scaffolding.html - */ - public $scaffold = false; - -/** - * Holds current methods of the controller. This is a list of all the methods reachable - * via URL. Modifying this array will allow you to change which methods can be reached. - * - * @var array - */ - public $methods = array(); - -/** - * This controller's primary model class name, the Inflector::singularize()'ed version of - * the controller's $name property. - * - * Example: For a controller named 'Comments', the modelClass would be 'Comment' - * - * @var string - */ - public $modelClass = null; - -/** - * This controller's model key name, an underscored version of the controller's $modelClass property. - * - * Example: For a controller named 'ArticleComments', the modelKey would be 'article_comment' - * - * @var string - */ - public $modelKey = null; - -/** - * Holds any validation errors produced by the last call of the validateErrors() method. - * Contains `false` if no validation errors happened. - * - * @var array|bool - */ - public $validationErrors = null; - -/** - * The class name of the parent class you wish to merge with. - * Typically this is AppController, but you may wish to merge vars with a different - * parent class. - * - * @var string - */ - protected $_mergeParent = 'AppController'; - -/** - * Instance of the CakeEventManager this controller is using - * to dispatch inner events. - * - * @var CakeEventManager - */ - protected $_eventManager = null; - -/** - * Constructor. - * - * @param CakeRequest $request Request object for this controller. Can be null for testing, - * but expect that features that use the request parameters will not work. - * @param CakeResponse $response Response object for this controller. - */ - public function __construct($request = null, $response = null) { - if ($this->name === null) { - $this->name = substr(get_class($this), 0, -10); - } - - if (!$this->viewPath) { - $this->viewPath = $this->name; - } - - $this->modelClass = Inflector::singularize($this->name); - $this->modelKey = Inflector::underscore($this->modelClass); - $this->Components = new ComponentCollection(); - - $childMethods = get_class_methods($this); - $parentMethods = get_class_methods('Controller'); - - $this->methods = array_diff($childMethods, $parentMethods); - - if ($request instanceof CakeRequest) { - $this->setRequest($request); - } - if ($response instanceof CakeResponse) { - $this->response = $response; - } - parent::__construct(); - } - -/** - * Provides backwards compatibility to avoid problems with empty and isset to alias properties. - * Lazy loads models using the loadModel() method if declared in $uses - * - * @param string $name Property name to check. - * @return bool - */ - public function __isset($name) { - switch ($name) { - case 'base': - case 'here': - case 'webroot': - case 'data': - case 'action': - case 'params': - return true; - } - - if (is_array($this->uses)) { - foreach ($this->uses as $modelClass) { - list($plugin, $class) = pluginSplit($modelClass, true); - if ($name === $class) { - return $this->loadModel($modelClass); - } - } - } - - if ($name === $this->modelClass) { - list($plugin, $class) = pluginSplit($name, true); - if (!$plugin) { - $plugin = $this->plugin ? $this->plugin . '.' : null; - } - return $this->loadModel($plugin . $this->modelClass); - } - - return false; - } - -/** - * Provides backwards compatibility access to the request object properties. - * Also provides the params alias. - * - * @param string $name The name of the requested value - * @return mixed The requested value for valid variables/aliases else null - */ - public function __get($name) { - switch ($name) { - case 'base': - case 'here': - case 'webroot': - case 'data': - return $this->request->{$name}; - case 'action': - return isset($this->request->params['action']) ? $this->request->params['action'] : ''; - case 'params': - return $this->request; - case 'paginate': - return $this->Components->load('Paginator')->settings; - } - - if (isset($this->{$name})) { - return $this->{$name}; - } - - return null; - } - -/** - * Provides backwards compatibility access for setting values to the request object. - * - * @param string $name Property name to set. - * @param mixed $value Value to set. - * @return void - */ - public function __set($name, $value) { - switch ($name) { - case 'base': - case 'here': - case 'webroot': - case 'data': - $this->request->{$name} = $value; - return; - case 'action': - $this->request->params['action'] = $value; - return; - case 'params': - $this->request->params = $value; - return; - case 'paginate': - $this->Components->load('Paginator')->settings = $value; - return; - } - $this->{$name} = $value; - } - -/** - * Sets the request objects and configures a number of controller properties - * based on the contents of the request. The properties that get set are - * - * - $this->request - To the $request parameter - * - $this->plugin - To the $request->params['plugin'] - * - $this->view - To the $request->params['action'] - * - $this->autoLayout - To the false if $request->params['bare']; is set. - * - $this->autoRender - To false if $request->params['return'] == 1 - * - $this->passedArgs - The the combined results of params['named'] and params['pass] - * - * @param CakeRequest $request Request instance. - * @return void - */ - public function setRequest(CakeRequest $request) { - $this->request = $request; - $this->plugin = isset($request->params['plugin']) ? Inflector::camelize($request->params['plugin']) : null; - $this->view = isset($request->params['action']) ? $request->params['action'] : null; - if (isset($request->params['pass']) && isset($request->params['named'])) { - $this->passedArgs = array_merge($request->params['pass'], $request->params['named']); - } - - if (!empty($request->params['return']) && $request->params['return'] == 1) { - $this->autoRender = false; - } - if (!empty($request->params['bare'])) { - $this->autoLayout = false; - } - } - -/** - * Dispatches the controller action. Checks that the action - * exists and isn't private. - * - * @param CakeRequest $request Request instance. - * @return mixed The resulting response. - * @throws PrivateActionException When actions are not public or prefixed by _ - * @throws MissingActionException When actions are not defined and scaffolding is - * not enabled. - */ - public function invokeAction(CakeRequest $request) { - try { - $method = new ReflectionMethod($this, $request->params['action']); - - if ($this->_isPrivateAction($method, $request)) { - throw new PrivateActionException(array( - 'controller' => $this->name . "Controller", - 'action' => $request->params['action'] - )); - } - return $method->invokeArgs($this, $request->params['pass']); - - } catch (ReflectionException $e) { - if ($this->scaffold !== false) { - return $this->_getScaffold($request); - } - throw new MissingActionException(array( - 'controller' => $this->name . "Controller", - 'action' => $request->params['action'] - )); - } - } - -/** - * Check if the request's action is marked as private, with an underscore, - * or if the request is attempting to directly accessing a prefixed action. - * - * @param ReflectionMethod $method The method to be invoked. - * @param CakeRequest $request The request to check. - * @return bool - */ - protected function _isPrivateAction(ReflectionMethod $method, CakeRequest $request) { - $privateAction = ( - $method->name[0] === '_' || - !$method->isPublic() || - !in_array($method->name, $this->methods) - ); - $prefixes = array_map('strtolower', Router::prefixes()); - - if (!$privateAction && !empty($prefixes)) { - if (empty($request->params['prefix']) && strpos($request->params['action'], '_') > 0) { - list($prefix) = explode('_', $request->params['action']); - $privateAction = in_array(strtolower($prefix), $prefixes); - } - } - return $privateAction; - } - -/** - * Returns a scaffold object to use for dynamically scaffolded controllers. - * - * @param CakeRequest $request Request instance. - * @return Scaffold - */ - protected function _getScaffold(CakeRequest $request) { - return new Scaffold($this, $request); - } - -/** - * Merge components, helpers, and uses vars from - * Controller::$_mergeParent and PluginAppController. - * - * @return void - */ - protected function _mergeControllerVars() { - $pluginController = $pluginDot = null; - $mergeParent = is_subclass_of($this, $this->_mergeParent); - $pluginVars = array(); - $appVars = array(); - - if (!empty($this->plugin)) { - $pluginController = $this->plugin . 'AppController'; - if (!is_subclass_of($this, $pluginController)) { - $pluginController = null; - } - $pluginDot = $this->plugin . '.'; - } - - if ($pluginController) { - $merge = array('components', 'helpers'); - $this->_mergeVars($merge, $pluginController); - } - - if ($mergeParent || !empty($pluginController)) { - $appVars = get_class_vars($this->_mergeParent); - $merge = array('components', 'helpers'); - $this->_mergeVars($merge, $this->_mergeParent, true); - } - - if ($this->uses === null) { - $this->uses = false; - } - if ($this->uses === true) { - $this->uses = array($pluginDot . $this->modelClass); - } - if (is_array($this->uses) && isset($appVars['uses']) && $appVars['uses'] === $this->uses) { - array_unshift($this->uses, $pluginDot . $this->modelClass); - } - if ($pluginController) { - $pluginVars = get_class_vars($pluginController); - } - if ($this->uses !== false) { - $this->_mergeUses($pluginVars); - $this->_mergeUses($appVars); - } else { - $this->uses = array(); - $this->modelClass = ''; - } - } - -/** - * Helper method for merging the $uses property together. - * - * Merges the elements not already in $this->uses into - * $this->uses. - * - * @param array $merge The data to merge in. - * @return void - */ - protected function _mergeUses($merge) { - if (!isset($merge['uses']) || $merge['uses'] === true || !is_array($this->uses)) { - return; - } - $this->uses = array_merge( - $this->uses, - array_diff($merge['uses'], $this->uses) - ); - } - -/** - * Returns a list of all events that will fire in the controller during its lifecycle. - * You can override this function to add your own listener callbacks - * - * @return array - */ - public function implementedEvents() { - return array( - 'Controller.initialize' => 'beforeFilter', - 'Controller.beforeRender' => 'beforeRender', - 'Controller.beforeRedirect' => array('callable' => 'beforeRedirect', 'passParams' => true), - 'Controller.shutdown' => 'afterFilter' - ); - } - -/** - * Loads Model classes based on the uses property - * see Controller::loadModel(); for more info. - * Loads Components and prepares them for initialization. - * - * @return mixed true if models found and instance created. - * @see Controller::loadModel() - * @link https://book.cakephp.org/2.0/en/controllers.html#Controller::constructClasses - * @throws MissingModelException - */ - public function constructClasses() { - $this->_mergeControllerVars(); - if ($this->uses) { - $this->uses = (array)$this->uses; - list(, $this->modelClass) = pluginSplit(reset($this->uses)); - } - $this->Components->init($this); - return true; - } - -/** - * Returns the CakeEventManager manager instance that is handling any callbacks. - * You can use this instance to register any new listeners or callbacks to the - * controller events, or create your own events and trigger them at will. - * - * @return CakeEventManager - */ - public function getEventManager() { - if (empty($this->_eventManager)) { - $this->_eventManager = new CakeEventManager(); - $this->_eventManager->attach($this->Components); - $this->_eventManager->attach($this); - } - return $this->_eventManager; - } - -/** - * Perform the startup process for this controller. - * Fire the Components and Controller callbacks in the correct order. - * - * - Initializes components, which fires their `initialize` callback - * - Calls the controller `beforeFilter`. - * - triggers Component `startup` methods. - * - * @return void - * @triggers Controller.initialize $this - * @triggers Controller.startup $this - */ - public function startupProcess() { - $this->getEventManager()->dispatch(new CakeEvent('Controller.initialize', $this)); - $this->getEventManager()->dispatch(new CakeEvent('Controller.startup', $this)); - } - -/** - * Perform the various shutdown processes for this controller. - * Fire the Components and Controller callbacks in the correct order. - * - * - triggers the component `shutdown` callback. - * - calls the Controller's `afterFilter` method. - * - * @return void - * @triggers Controller.shutdown $this - */ - public function shutdownProcess() { - $this->getEventManager()->dispatch(new CakeEvent('Controller.shutdown', $this)); - } - -/** - * Queries & sets valid HTTP response codes & messages. - * - * @param int|array $code If $code is an integer, then the corresponding code/message is - * returned if it exists, null if it does not exist. If $code is an array, - * then the 'code' and 'message' keys of each nested array are added to the default - * HTTP codes. Example: - * - * httpCodes(404); // returns array(404 => 'Not Found') - * - * httpCodes(array( - * 701 => 'Unicorn Moved', - * 800 => 'Unexpected Minotaur' - * )); // sets these new values, and returns true - * - * @return array|null|true Associative array of the HTTP codes as keys, and the message - * strings as values, or null of the given $code does not exist. - * @deprecated 3.0.0 Since 2.4. Will be removed in 3.0. Use CakeResponse::httpCodes(). - */ - public function httpCodes($code = null) { - return $this->response->httpCodes($code); - } - -/** - * Loads and instantiates models required by this controller. - * If the model is non existent, it will throw a missing database table error, as CakePHP generates - * dynamic models for the time being. - * - * @param string $modelClass Name of model class to load - * @param int|string $id Initial ID the instanced model class should have - * @return bool True if the model was found - * @throws MissingModelException if the model class cannot be found. - */ - public function loadModel($modelClass = null, $id = null) { - if ($modelClass === null) { - $modelClass = $this->modelClass; - } - - $this->uses = ($this->uses) ? (array)$this->uses : array(); - if (!in_array($modelClass, $this->uses, true)) { - $this->uses[] = $modelClass; - } - - list($plugin, $modelClass) = pluginSplit($modelClass, true); - - $this->{$modelClass} = ClassRegistry::init(array( - 'class' => $plugin . $modelClass, 'alias' => $modelClass, 'id' => $id - )); - if (!$this->{$modelClass}) { - throw new MissingModelException($modelClass); - } - return true; - } - -/** - * Redirects to given $url, after turning off $this->autoRender. - * Script execution is halted after the redirect. - * - * @param string|array $url A string or array-based URL pointing to another location within the app, - * or an absolute URL - * @param int|array|null|string $status HTTP status code (eg: 301). Defaults to 302 when null is passed. - * @param bool $exit If true, exit() will be called after the redirect - * @return CakeResponse|null - * @triggers Controller.beforeRedirect $this, array($url, $status, $exit) - * @link https://book.cakephp.org/2.0/en/controllers.html#Controller::redirect - */ - public function redirect($url, $status = null, $exit = true) { - $this->autoRender = false; - - if (is_array($status)) { - extract($status, EXTR_OVERWRITE); - } - $event = new CakeEvent('Controller.beforeRedirect', $this, array($url, $status, $exit)); - - list($event->break, $event->breakOn, $event->collectReturn) = array(true, false, true); - $this->getEventManager()->dispatch($event); - - if ($event->isStopped()) { - return null; - } - $response = $event->result; - extract($this->_parseBeforeRedirect($response, $url, $status, $exit), EXTR_OVERWRITE); - - if ($url !== null) { - $this->response->header('Location', Router::url($url, true)); - } - - if (is_string($status)) { - $codes = array_flip($this->response->httpCodes()); - if (isset($codes[$status])) { - $status = $codes[$status]; - } - } - - if ($status === null) { - $status = 302; - } - $this->response->statusCode($status); - - if ($exit) { - $this->response->send(); - $this->_stop(); - } - - return $this->response; - } - -/** - * Parse beforeRedirect Response - * - * @param mixed $response Response from beforeRedirect callback - * @param string|array $url The same value of beforeRedirect - * @param int $status The same value of beforeRedirect - * @param bool $exit The same value of beforeRedirect - * @return array Array with keys url, status and exit - */ - protected function _parseBeforeRedirect($response, $url, $status, $exit) { - if (is_array($response) && array_key_exists(0, $response)) { - foreach ($response as $resp) { - if (is_array($resp) && isset($resp['url'])) { - extract($resp, EXTR_OVERWRITE); - } elseif ($resp !== null) { - $url = $resp; - } - } - } elseif (is_array($response)) { - extract($response, EXTR_OVERWRITE); - } - return compact('url', 'status', 'exit'); - } - -/** - * Convenience and object wrapper method for CakeResponse::header(). - * - * @param string $status The header message that is being set. - * @return void - * @deprecated 3.0.0 Will be removed in 3.0. Use CakeResponse::header(). - */ - public function header($status) { - $this->response->header($status); - } - -/** - * Saves a variable for use inside a view template. - * - * @param string|array $one A string or an array of data. - * @param mixed $two Value in case $one is a string (which then works as the key). - * Unused if $one is an associative array, otherwise serves as the values to $one's keys. - * @return void - * @link https://book.cakephp.org/2.0/en/controllers.html#interacting-with-views - */ - public function set($one, $two = null) { - if (is_array($one)) { - if (is_array($two)) { - $data = array_combine($one, $two); - } else { - $data = $one; - } - } else { - $data = array($one => $two); - } - $this->viewVars = $data + $this->viewVars; - } - -/** - * Internally redirects one action to another. Does not perform another HTTP request unlike Controller::redirect() - * - * Examples: - * - * ``` - * setAction('another_action'); - * setAction('action_with_parameters', $parameter1); - * ``` - * - * @param string $action The new action to be 'redirected' to. - * Any other parameters passed to this method will be passed as parameters to the new action. - * @return mixed Returns the return value of the called action - */ - public function setAction($action) { - $this->request->params['action'] = $action; - $this->view = $action; - $args = func_get_args(); - unset($args[0]); - return call_user_func_array(array(&$this, $action), $args); - } - -/** - * Returns number of errors in a submitted FORM. - * - * @return int Number of errors - * @deprecated 3.0.0 This method will be removed in 3.0 - */ - public function validate() { - $args = func_get_args(); - $errors = call_user_func_array(array(&$this, 'validateErrors'), $args); - - if ($errors === false) { - return 0; - } - return count($errors); - } - -/** - * Validates models passed by parameters. Takes a list of models as a variable argument. - * Example: - * - * `$errors = $this->validateErrors($this->Article, $this->User);` - * - * @return array|false Validation errors, or false if none - * @deprecated 3.0.0 This method will be removed in 3.0 - */ - public function validateErrors() { - $objects = func_get_args(); - - if (empty($objects)) { - return false; - } - - $errors = array(); - foreach ($objects as $object) { - if (isset($this->{$object->alias})) { - $object = $this->{$object->alias}; - } - $object->set($object->data); - $errors = array_merge($errors, $object->invalidFields()); - } - - return $this->validationErrors = (!empty($errors) ? $errors : false); - } - -/** - * Instantiates the correct view class, hands it its data, and uses it to render the view output. - * - * @param bool|string $view View to use for rendering - * @param string $layout Layout to use - * @return CakeResponse A response object containing the rendered view. - * @triggers Controller.beforeRender $this - * @link https://book.cakephp.org/2.0/en/controllers.html#Controller::render - */ - public function render($view = null, $layout = null) { - $event = new CakeEvent('Controller.beforeRender', $this); - $this->getEventManager()->dispatch($event); - if ($event->isStopped()) { - $this->autoRender = false; - return $this->response; - } - - if (!empty($this->uses) && is_array($this->uses)) { - foreach ($this->uses as $model) { - list($plugin, $className) = pluginSplit($model); - $this->request->params['models'][$className] = compact('plugin', 'className'); - } - } - - $this->View = $this->_getViewObject(); - - $models = ClassRegistry::keys(); - foreach ($models as $currentModel) { - $currentObject = ClassRegistry::getObject($currentModel); - if ($currentObject instanceof Model) { - $className = get_class($currentObject); - list($plugin) = pluginSplit(App::location($className)); - $this->request->params['models'][$currentObject->alias] = compact('plugin', 'className'); - $this->View->validationErrors[$currentObject->alias] =& $currentObject->validationErrors; - } - } - - $this->autoRender = false; - $this->response->body($this->View->render($view, $layout)); - return $this->response; - } - -/** - * Returns the referring URL for this request. - * - * @param string $default Default URL to use if HTTP_REFERER cannot be read from headers - * @param bool $local If true, restrict referring URLs to local server - * @return string Referring URL - * @link https://book.cakephp.org/2.0/en/controllers.html#Controller::referer - */ - public function referer($default = null, $local = false) { - if (!$this->request) { - return '/'; - } - - $referer = $this->request->referer($local); - if ($referer === '/' && $default && $default !== $referer) { - return Router::url($default, !$local); - } - return $referer; - } - -/** - * Forces the user's browser not to cache the results of the current request. - * - * @return void - * @link https://book.cakephp.org/2.0/en/controllers.html#Controller::disableCache - * @deprecated 3.0.0 Will be removed in 3.0. Use CakeResponse::disableCache(). - */ - public function disableCache() { - $this->response->disableCache(); - } - -/** - * Shows a message to the user for $pause seconds, then redirects to $url. - * Uses flash.ctp as the default layout for the message. - * Does not work if the current debug level is higher than 0. - * - * @param string $message Message to display to the user - * @param string|array $url Relative string or array-based URL to redirect to after the time expires - * @param int $pause Time to show the message - * @param string $layout Layout you want to use, defaults to 'flash' - * @return void - * @link https://book.cakephp.org/2.0/en/controllers.html#Controller::flash - * @deprecated 3.0.0 Will be removed in 3.0. Use Flash::set() with version 2.7+ or Session::setFlash() prior to 2.7. - */ - public function flash($message, $url, $pause = 1, $layout = 'flash') { - $this->autoRender = false; - $this->set('url', Router::url($url)); - $this->set('message', $message); - $this->set('pause', $pause); - $this->set('pageTitle', $message); - $this->render(false, $layout); - } - -/** - * Converts POST'ed form data to a model conditions array. - * - * If combined with SecurityComponent these conditions could be suitable - * for use in a Model::find() call. Without SecurityComponent this method - * is vulnerable creating conditions containing SQL injection. While we - * attempt to raise exceptions. - * - * @param array $data POST'ed data organized by model and field - * @param string|array $op A string containing an SQL comparison operator, or an array matching operators - * to fields - * @param string $bool SQL boolean operator: AND, OR, XOR, etc. - * @param bool $exclusive If true, and $op is an array, fields not included in $op will not be - * included in the returned conditions - * @return array|null An array of model conditions - * @deprecated 3.0.0 Will be removed in 3.0. - * @throws RuntimeException when unsafe operators are found. - */ - public function postConditions($data = array(), $op = null, $bool = 'AND', $exclusive = false) { - if (!is_array($data) || empty($data)) { - if (!empty($this->request->data)) { - $data = $this->request->data; - } else { - return null; - } - } - $cond = array(); - - if ($op === null) { - $op = ''; - } - - $allowedChars = '#[^a-zA-Z0-9_ ]#'; - $arrayOp = is_array($op); - foreach ($data as $model => $fields) { - if (preg_match($allowedChars, $model)) { - throw new RuntimeException("Unsafe operator found in {$model}"); - } - foreach ($fields as $field => $value) { - if (preg_match($allowedChars, $field)) { - throw new RuntimeException("Unsafe operator found in {$model}.{$field}"); - } - $key = $model . '.' . $field; - $fieldOp = $op; - if ($arrayOp) { - if (array_key_exists($key, $op)) { - $fieldOp = $op[$key]; - } elseif (array_key_exists($field, $op)) { - $fieldOp = $op[$field]; - } else { - $fieldOp = false; - } - } - if ($exclusive && $fieldOp === false) { - continue; - } - $fieldOp = strtoupper(trim($fieldOp)); - if ($fieldOp === 'LIKE') { - $key = $key . ' LIKE'; - $value = '%' . $value . '%'; - } elseif ($fieldOp && $fieldOp !== '=') { - $key = $key . ' ' . $fieldOp; - } - $cond[$key] = $value; - } - } - if ($bool && strtoupper($bool) !== 'AND') { - $cond = array($bool => $cond); - } - return $cond; - } - -/** - * Handles automatic pagination of model records. - * - * @param Model|string $object Model to paginate (e.g: model instance, or 'Model', or 'Model.InnerModel') - * @param string|array $scope Conditions to use while paginating - * @param array $whitelist List of allowed options for paging - * @return array Model query results - * @link https://book.cakephp.org/2.0/en/controllers.html#Controller::paginate - */ - public function paginate($object = null, $scope = array(), $whitelist = array()) { - return $this->Components->load('Paginator', $this->paginate)->paginate($object, $scope, $whitelist); - } - -/** - * Called before the controller action. You can use this method to configure and customize components - * or perform logic that needs to happen before each controller action. - * - * @return void - * @link https://book.cakephp.org/2.0/en/controllers.html#request-life-cycle-callbacks - */ - public function beforeFilter() { - } - -/** - * Called after the controller action is run, but before the view is rendered. You can use this method - * to perform logic or set view variables that are required on every request. - * - * @return void - * @link https://book.cakephp.org/2.0/en/controllers.html#request-life-cycle-callbacks - */ - public function beforeRender() { - } - -/** - * The beforeRedirect method is invoked when the controller's redirect method is called but before any - * further action. - * - * If this method returns false the controller will not continue on to redirect the request. - * The $url, $status and $exit variables have same meaning as for the controller's method. You can also - * return a string which will be interpreted as the URL to redirect to or return associative array with - * key 'url' and optionally 'status' and 'exit'. - * - * @param string|array $url A string or array-based URL pointing to another location within the app, - * or an absolute URL - * @param int $status Optional HTTP status code (eg: 404) - * @param bool $exit If true, exit() will be called after the redirect - * @return mixed - * false to stop redirection event, - * string controllers a new redirection URL or - * array with the keys url, status and exit to be used by the redirect method. - * @link https://book.cakephp.org/2.0/en/controllers.html#request-life-cycle-callbacks - */ - public function beforeRedirect($url, $status = null, $exit = true) { - } - -/** - * Called after the controller action is run and rendered. - * - * @return void - * @link https://book.cakephp.org/2.0/en/controllers.html#request-life-cycle-callbacks - */ - public function afterFilter() { - } - -/** - * This method should be overridden in child classes. - * - * @param string $method name of method called example index, edit, etc. - * @return bool Success - * @link https://book.cakephp.org/2.0/en/controllers.html#callbacks - */ - public function beforeScaffold($method) { - return true; - } - -/** - * Alias to beforeScaffold() - * - * @param string $method Method name. - * @return bool - * @see Controller::beforeScaffold() - * @deprecated 3.0.0 Will be removed in 3.0. - */ - protected function _beforeScaffold($method) { - return $this->beforeScaffold($method); - } - -/** - * This method should be overridden in child classes. - * - * @param string $method name of method called either edit or update. - * @return bool Success - * @link https://book.cakephp.org/2.0/en/controllers.html#callbacks - */ - public function afterScaffoldSave($method) { - return true; - } - -/** - * Alias to afterScaffoldSave() - * - * @param string $method Method name. - * @return bool - * @see Controller::afterScaffoldSave() - * @deprecated 3.0.0 Will be removed in 3.0. - */ - protected function _afterScaffoldSave($method) { - return $this->afterScaffoldSave($method); - } - -/** - * This method should be overridden in child classes. - * - * @param string $method name of method called either edit or update. - * @return bool Success - * @link https://book.cakephp.org/2.0/en/controllers.html#callbacks - */ - public function afterScaffoldSaveError($method) { - return true; - } - -/** - * Alias to afterScaffoldSaveError() - * - * @param string $method Method name. - * @return bool - * @see Controller::afterScaffoldSaveError() - * @deprecated 3.0.0 Will be removed in 3.0. - */ - protected function _afterScaffoldSaveError($method) { - return $this->afterScaffoldSaveError($method); - } - -/** - * This method should be overridden in child classes. - * If not it will render a scaffold error. - * Method MUST return true in child classes - * - * @param string $method name of method called example index, edit, etc. - * @return bool Success - * @link https://book.cakephp.org/2.0/en/controllers.html#callbacks - */ - public function scaffoldError($method) { - return false; - } - -/** - * Alias to scaffoldError() - * - * @param string $method Method name. - * @return bool - * @see Controller::scaffoldError() - * @deprecated 3.0.0 Will be removed in 3.0. - */ - protected function _scaffoldError($method) { - return $this->scaffoldError($method); - } - -/** - * Constructs the view class instance based on the controller property - * - * @return View - */ - protected function _getViewObject() { - $viewClass = $this->viewClass; - if ($this->viewClass !== 'View') { - list($plugin, $viewClass) = pluginSplit($viewClass, true); - $viewClass = $viewClass . 'View'; - App::uses($viewClass, $plugin . 'View'); - } - - return new $viewClass($this); - } +class Controller extends CakeObject implements CakeEventListener +{ + + /** + * The name of this controller. Controller names are plural, named after the model they manipulate. + * + * @var string + * @link https://book.cakephp.org/2.0/en/controllers.html#controller-attributes + */ + public $name = null; + + /** + * An array containing the class names of models this controller uses. + * + * Example: `public $uses = array('Product', 'Post', 'Comment');` + * + * Can be set to several values to express different options: + * + * - `true` Use the default inflected model name. + * - `array()` Use only models defined in the parent class. + * - `false` Use no models at all, do not merge with parent class either. + * - `array('Post', 'Comment')` Use only the Post and Comment models. Models + * Will also be merged with the parent class. + * + * The default value is `true`. + * + * @var bool|array + * @link https://book.cakephp.org/2.0/en/controllers.html#components-helpers-and-uses + */ + public $uses = true; + + /** + * An array containing the names of helpers this controller uses. The array elements should + * not contain the "Helper" part of the class name. + * + * Example: `public $helpers = array('Html', 'Js', 'Time', 'Ajax');` + * + * @var mixed + * @link https://book.cakephp.org/2.0/en/controllers.html#components-helpers-and-uses + */ + public $helpers = []; + + /** + * An instance of a CakeRequest object that contains information about the current request. + * This object contains all the information about a request and several methods for reading + * additional information about the request. + * + * @var CakeRequest + * @link https://book.cakephp.org/2.0/en/controllers/request-response.html#cakerequest + */ + public $request; + + /** + * An instance of a CakeResponse object that contains information about the impending response + * + * @var CakeResponse + * @link https://book.cakephp.org/2.0/en/controllers/request-response.html#cakeresponse + */ + public $response; + /** + * The name of the views subfolder containing views for this controller. + * + * @var string + */ + public $viewPath = null; + /** + * The name of the layouts subfolder containing layouts for this controller. + * + * @var string + */ + public $layoutPath = null; + /** + * Contains variables to be handed to the view. + * + * @var array + */ + public $viewVars = []; + /** + * The name of the view file to render. The name specified + * is the filename in /app/View/ without the .ctp extension. + * + * @var string + */ + public $view = null; + /** + * The name of the layout file to render the view inside of. The name specified + * is the filename of the layout in /app/View/Layouts without the .ctp + * extension. If `false` then no layout is rendered. + * + * @var string|bool + */ + public $layout = 'default'; + /** + * Set to true to automatically render the view + * after action logic. + * + * @var bool + */ + public $autoRender = true; + /** + * Set to true to automatically render the layout around views. + * + * @var bool + */ + public $autoLayout = true; + /** + * Instance of ComponentCollection used to handle callbacks. + * + * @var ComponentCollection + */ + public $Components = null; + /** + * Array containing the names of components this controller uses. Component names + * should not contain the "Component" portion of the class name. + * + * Example: `public $components = array('Session', 'RequestHandler', 'Acl');` + * + * @var array + * @link https://book.cakephp.org/2.0/en/controllers/components.html + */ + public $components = ['Session', 'Flash']; + /** + * The name of the View class this controller sends output to. + * + * @var string + */ + public $viewClass = 'View'; + /** + * Instance of the View created during rendering. Won't be set until after + * Controller::render() is called. + * + * @var View + */ + public $View; + /** + * File extension for view templates. Defaults to CakePHP's conventional ".ctp". + * + * @var string + */ + public $ext = '.ctp'; + /** + * Automatically set to the name of a plugin. + * + * @var string + */ + public $plugin = null; + /** + * Used to define methods a controller that will be cached. To cache a + * single action, the value is set to an array containing keys that match + * action names and values that denote cache expiration times (in seconds). + * + * Example: + * + * ``` + * public $cacheAction = array( + * 'view/23/' => 21600, + * 'recalled/' => 86400 + * ); + * ``` + * + * $cacheAction can also be set to a strtotime() compatible string. This + * marks all the actions in the controller for view caching. + * + * @var mixed + * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/cache.html#additional-configuration-options + */ + public $cacheAction = false; + /** + * Holds all params passed and named. + * + * @var mixed + */ + public $passedArgs = []; + /** + * Triggers Scaffolding + * + * @var mixed + * @link https://book.cakephp.org/2.0/en/controllers/scaffolding.html + */ + public $scaffold = false; + /** + * Holds current methods of the controller. This is a list of all the methods reachable + * via URL. Modifying this array will allow you to change which methods can be reached. + * + * @var array + */ + public $methods = []; + /** + * This controller's primary model class name, the Inflector::singularize()'ed version of + * the controller's $name property. + * + * Example: For a controller named 'Comments', the modelClass would be 'Comment' + * + * @var string + */ + public $modelClass = null; + /** + * This controller's model key name, an underscored version of the controller's $modelClass property. + * + * Example: For a controller named 'ArticleComments', the modelKey would be 'article_comment' + * + * @var string + */ + public $modelKey = null; + /** + * Holds any validation errors produced by the last call of the validateErrors() method. + * Contains `false` if no validation errors happened. + * + * @var array|bool + */ + public $validationErrors = null; + /** + * The class name to use for creating the response object. + * + * @var string + */ + protected $_responseClass = 'CakeResponse'; + /** + * The class name of the parent class you wish to merge with. + * Typically this is AppController, but you may wish to merge vars with a different + * parent class. + * + * @var string + */ + protected $_mergeParent = 'AppController'; + + /** + * Instance of the CakeEventManager this controller is using + * to dispatch inner events. + * + * @var CakeEventManager + */ + protected $_eventManager = null; + + /** + * Constructor. + * + * @param CakeRequest $request Request object for this controller. Can be null for testing, + * but expect that features that use the request parameters will not work. + * @param CakeResponse $response Response object for this controller. + */ + public function __construct($request = null, $response = null) + { + if ($this->name === null) { + $this->name = substr(get_class($this), 0, -10); + } + + if (!$this->viewPath) { + $this->viewPath = $this->name; + } + + $this->modelClass = Inflector::singularize($this->name); + $this->modelKey = Inflector::underscore($this->modelClass); + $this->Components = new ComponentCollection(); + + $childMethods = get_class_methods($this); + $parentMethods = get_class_methods('Controller'); + + $this->methods = array_diff($childMethods, $parentMethods); + + if ($request instanceof CakeRequest) { + $this->setRequest($request); + } + if ($response instanceof CakeResponse) { + $this->response = $response; + } + parent::__construct(); + } + + /** + * Sets the request objects and configures a number of controller properties + * based on the contents of the request. The properties that get set are + * + * - $this->request - To the $request parameter + * - $this->plugin - To the $request->params['plugin'] + * - $this->view - To the $request->params['action'] + * - $this->autoLayout - To the false if $request->params['bare']; is set. + * - $this->autoRender - To false if $request->params['return'] == 1 + * - $this->passedArgs - The the combined results of params['named'] and params['pass] + * + * @param CakeRequest $request Request instance. + * @return void + */ + public function setRequest(CakeRequest $request) + { + $this->request = $request; + $this->plugin = isset($request->params['plugin']) ? Inflector::camelize($request->params['plugin']) : null; + $this->view = isset($request->params['action']) ? $request->params['action'] : null; + if (isset($request->params['pass']) && isset($request->params['named'])) { + $this->passedArgs = array_merge($request->params['pass'], $request->params['named']); + } + + if (!empty($request->params['return']) && $request->params['return'] == 1) { + $this->autoRender = false; + } + if (!empty($request->params['bare'])) { + $this->autoLayout = false; + } + } + + /** + * Provides backwards compatibility to avoid problems with empty and isset to alias properties. + * Lazy loads models using the loadModel() method if declared in $uses + * + * @param string $name Property name to check. + * @return bool + */ + public function __isset($name) + { + switch ($name) { + case 'base': + case 'here': + case 'webroot': + case 'data': + case 'action': + case 'params': + return true; + } + + if (is_array($this->uses)) { + foreach ($this->uses as $modelClass) { + list($plugin, $class) = pluginSplit($modelClass, true); + if ($name === $class) { + return $this->loadModel($modelClass); + } + } + } + + if ($name === $this->modelClass) { + list($plugin, $class) = pluginSplit($name, true); + if (!$plugin) { + $plugin = $this->plugin ? $this->plugin . '.' : null; + } + return $this->loadModel($plugin . $this->modelClass); + } + + return false; + } + + /** + * Loads and instantiates models required by this controller. + * If the model is non existent, it will throw a missing database table error, as CakePHP generates + * dynamic models for the time being. + * + * @param string $modelClass Name of model class to load + * @param int|string $id Initial ID the instanced model class should have + * @return bool True if the model was found + * @throws MissingModelException if the model class cannot be found. + */ + public function loadModel($modelClass = null, $id = null) + { + if ($modelClass === null) { + $modelClass = $this->modelClass; + } + + $this->uses = ($this->uses) ? (array)$this->uses : []; + if (!in_array($modelClass, $this->uses, true)) { + $this->uses[] = $modelClass; + } + + list($plugin, $modelClass) = pluginSplit($modelClass, true); + + $this->{$modelClass} = ClassRegistry::init([ + 'class' => $plugin . $modelClass, 'alias' => $modelClass, 'id' => $id + ]); + if (!$this->{$modelClass}) { + throw new MissingModelException($modelClass); + } + return true; + } + + /** + * Provides backwards compatibility access to the request object properties. + * Also provides the params alias. + * + * @param string $name The name of the requested value + * @return mixed The requested value for valid variables/aliases else null + */ + public function __get($name) + { + switch ($name) { + case 'base': + case 'here': + case 'webroot': + case 'data': + return $this->request->{$name}; + case 'action': + return isset($this->request->params['action']) ? $this->request->params['action'] : ''; + case 'params': + return $this->request; + case 'paginate': + return $this->Components->load('Paginator')->settings; + } + + if (isset($this->{$name})) { + return $this->{$name}; + } + + return null; + } + + /** + * Provides backwards compatibility access for setting values to the request object. + * + * @param string $name Property name to set. + * @param mixed $value Value to set. + * @return void + */ + public function __set($name, $value) + { + switch ($name) { + case 'base': + case 'here': + case 'webroot': + case 'data': + $this->request->{$name} = $value; + return; + case 'action': + $this->request->params['action'] = $value; + return; + case 'params': + $this->request->params = $value; + return; + case 'paginate': + $this->Components->load('Paginator')->settings = $value; + return; + } + $this->{$name} = $value; + } + + /** + * Dispatches the controller action. Checks that the action + * exists and isn't private. + * + * @param CakeRequest $request Request instance. + * @return mixed The resulting response. + * @throws PrivateActionException When actions are not public or prefixed by _ + * @throws MissingActionException When actions are not defined and scaffolding is + * not enabled. + */ + public function invokeAction(CakeRequest $request) + { + try { + $method = new ReflectionMethod($this, $request->params['action']); + + if ($this->_isPrivateAction($method, $request)) { + throw new PrivateActionException([ + 'controller' => $this->name . "Controller", + 'action' => $request->params['action'] + ]); + } + return $method->invokeArgs($this, $request->params['pass']); + + } catch (ReflectionException $e) { + if ($this->scaffold !== false) { + return $this->_getScaffold($request); + } + throw new MissingActionException([ + 'controller' => $this->name . "Controller", + 'action' => $request->params['action'] + ]); + } + } + + /** + * Check if the request's action is marked as private, with an underscore, + * or if the request is attempting to directly accessing a prefixed action. + * + * @param ReflectionMethod $method The method to be invoked. + * @param CakeRequest $request The request to check. + * @return bool + */ + protected function _isPrivateAction(ReflectionMethod $method, CakeRequest $request) + { + $privateAction = ( + $method->name[0] === '_' || + !$method->isPublic() || + !in_array($method->name, $this->methods) + ); + $prefixes = array_map('strtolower', Router::prefixes()); + + if (!$privateAction && !empty($prefixes)) { + if (empty($request->params['prefix']) && strpos($request->params['action'], '_') > 0) { + list($prefix) = explode('_', $request->params['action']); + $privateAction = in_array(strtolower($prefix), $prefixes); + } + } + return $privateAction; + } + + /** + * Returns a scaffold object to use for dynamically scaffolded controllers. + * + * @param CakeRequest $request Request instance. + * @return Scaffold + */ + protected function _getScaffold(CakeRequest $request) + { + return new Scaffold($this, $request); + } + + /** + * Returns a list of all events that will fire in the controller during its lifecycle. + * You can override this function to add your own listener callbacks + * + * @return array + */ + public function implementedEvents() + { + return [ + 'Controller.initialize' => 'beforeFilter', + 'Controller.beforeRender' => 'beforeRender', + 'Controller.beforeRedirect' => ['callable' => 'beforeRedirect', 'passParams' => true], + 'Controller.shutdown' => 'afterFilter' + ]; + } + + /** + * Loads Model classes based on the uses property + * see Controller::loadModel(); for more info. + * Loads Components and prepares them for initialization. + * + * @return mixed true if models found and instance created. + * @throws MissingModelException + * @link https://book.cakephp.org/2.0/en/controllers.html#Controller::constructClasses + * @see Controller::loadModel() + */ + public function constructClasses() + { + $this->_mergeControllerVars(); + if ($this->uses) { + $this->uses = (array)$this->uses; + list(, $this->modelClass) = pluginSplit(reset($this->uses)); + } + $this->Components->init($this); + return true; + } + + /** + * Merge components, helpers, and uses vars from + * Controller::$_mergeParent and PluginAppController. + * + * @return void + */ + protected function _mergeControllerVars() + { + $pluginController = $pluginDot = null; + $mergeParent = is_subclass_of($this, $this->_mergeParent); + $pluginVars = []; + $appVars = []; + + if (!empty($this->plugin)) { + $pluginController = $this->plugin . 'AppController'; + if (!is_subclass_of($this, $pluginController)) { + $pluginController = null; + } + $pluginDot = $this->plugin . '.'; + } + + if ($pluginController) { + $merge = ['components', 'helpers']; + $this->_mergeVars($merge, $pluginController); + } + + if ($mergeParent || !empty($pluginController)) { + $appVars = get_class_vars($this->_mergeParent); + $merge = ['components', 'helpers']; + $this->_mergeVars($merge, $this->_mergeParent, true); + } + + if ($this->uses === null) { + $this->uses = false; + } + if ($this->uses === true) { + $this->uses = [$pluginDot . $this->modelClass]; + } + if (is_array($this->uses) && isset($appVars['uses']) && $appVars['uses'] === $this->uses) { + array_unshift($this->uses, $pluginDot . $this->modelClass); + } + if ($pluginController) { + $pluginVars = get_class_vars($pluginController); + } + if ($this->uses !== false) { + $this->_mergeUses($pluginVars); + $this->_mergeUses($appVars); + } else { + $this->uses = []; + $this->modelClass = ''; + } + } + + /** + * Helper method for merging the $uses property together. + * + * Merges the elements not already in $this->uses into + * $this->uses. + * + * @param array $merge The data to merge in. + * @return void + */ + protected function _mergeUses($merge) + { + if (!isset($merge['uses']) || $merge['uses'] === true || !is_array($this->uses)) { + return; + } + $this->uses = array_merge( + $this->uses, + array_diff($merge['uses'], $this->uses) + ); + } + + /** + * Perform the startup process for this controller. + * Fire the Components and Controller callbacks in the correct order. + * + * - Initializes components, which fires their `initialize` callback + * - Calls the controller `beforeFilter`. + * - triggers Component `startup` methods. + * + * @return void + * @triggers Controller.initialize $this + * @triggers Controller.startup $this + */ + public function startupProcess() + { + $this->getEventManager()->dispatch(new CakeEvent('Controller.initialize', $this)); + $this->getEventManager()->dispatch(new CakeEvent('Controller.startup', $this)); + } + + /** + * Returns the CakeEventManager manager instance that is handling any callbacks. + * You can use this instance to register any new listeners or callbacks to the + * controller events, or create your own events and trigger them at will. + * + * @return CakeEventManager + */ + public function getEventManager() + { + if (empty($this->_eventManager)) { + $this->_eventManager = new CakeEventManager(); + $this->_eventManager->attach($this->Components); + $this->_eventManager->attach($this); + } + return $this->_eventManager; + } + + /** + * Perform the various shutdown processes for this controller. + * Fire the Components and Controller callbacks in the correct order. + * + * - triggers the component `shutdown` callback. + * - calls the Controller's `afterFilter` method. + * + * @return void + * @triggers Controller.shutdown $this + */ + public function shutdownProcess() + { + $this->getEventManager()->dispatch(new CakeEvent('Controller.shutdown', $this)); + } + + /** + * Queries & sets valid HTTP response codes & messages. + * + * @param int|array $code If $code is an integer, then the corresponding code/message is + * returned if it exists, null if it does not exist. If $code is an array, + * then the 'code' and 'message' keys of each nested array are added to the default + * HTTP codes. Example: + * + * httpCodes(404); // returns array(404 => 'Not Found') + * + * httpCodes(array( + * 701 => 'Unicorn Moved', + * 800 => 'Unexpected Minotaur' + * )); // sets these new values, and returns true + * + * @return array|null|true Associative array of the HTTP codes as keys, and the message + * strings as values, or null of the given $code does not exist. + * @deprecated 3.0.0 Since 2.4. Will be removed in 3.0. Use CakeResponse::httpCodes(). + */ + public function httpCodes($code = null) + { + return $this->response->httpCodes($code); + } + + /** + * Redirects to given $url, after turning off $this->autoRender. + * Script execution is halted after the redirect. + * + * @param string|array $url A string or array-based URL pointing to another location within the app, + * or an absolute URL + * @param int|array|null|string $status HTTP status code (eg: 301). Defaults to 302 when null is passed. + * @param bool $exit If true, exit() will be called after the redirect + * @return CakeResponse|null + * @triggers Controller.beforeRedirect $this, array($url, $status, $exit) + * @link https://book.cakephp.org/2.0/en/controllers.html#Controller::redirect + */ + public function redirect($url, $status = null, $exit = true) + { + $this->autoRender = false; + + if (is_array($status)) { + extract($status, EXTR_OVERWRITE); + } + $event = new CakeEvent('Controller.beforeRedirect', $this, [$url, $status, $exit]); + + list($event->break, $event->breakOn, $event->collectReturn) = [true, false, true]; + $this->getEventManager()->dispatch($event); + + if ($event->isStopped()) { + return null; + } + $response = $event->result; + extract($this->_parseBeforeRedirect($response, $url, $status, $exit), EXTR_OVERWRITE); + + if ($url !== null) { + $this->response->header('Location', Router::url($url, true)); + } + + if (is_string($status)) { + $codes = array_flip($this->response->httpCodes()); + if (isset($codes[$status])) { + $status = $codes[$status]; + } + } + + if ($status === null) { + $status = 302; + } + $this->response->statusCode($status); + + if ($exit) { + $this->response->send(); + $this->_stop(); + } + + return $this->response; + } + + /** + * Parse beforeRedirect Response + * + * @param mixed $response Response from beforeRedirect callback + * @param string|array $url The same value of beforeRedirect + * @param int $status The same value of beforeRedirect + * @param bool $exit The same value of beforeRedirect + * @return array Array with keys url, status and exit + */ + protected function _parseBeforeRedirect($response, $url, $status, $exit) + { + if (is_array($response) && array_key_exists(0, $response)) { + foreach ($response as $resp) { + if (is_array($resp) && isset($resp['url'])) { + extract($resp, EXTR_OVERWRITE); + } else if ($resp !== null) { + $url = $resp; + } + } + } else if (is_array($response)) { + extract($response, EXTR_OVERWRITE); + } + return compact('url', 'status', 'exit'); + } + + /** + * Convenience and object wrapper method for CakeResponse::header(). + * + * @param string $status The header message that is being set. + * @return void + * @deprecated 3.0.0 Will be removed in 3.0. Use CakeResponse::header(). + */ + public function header($status) + { + $this->response->header($status); + } + + /** + * Internally redirects one action to another. Does not perform another HTTP request unlike Controller::redirect() + * + * Examples: + * + * ``` + * setAction('another_action'); + * setAction('action_with_parameters', $parameter1); + * ``` + * + * @param string $action The new action to be 'redirected' to. + * Any other parameters passed to this method will be passed as parameters to the new action. + * @return mixed Returns the return value of the called action + */ + public function setAction($action) + { + $this->request->params['action'] = $action; + $this->view = $action; + $args = func_get_args(); + unset($args[0]); + return call_user_func_array([&$this, $action], $args); + } + + /** + * Returns number of errors in a submitted FORM. + * + * @return int Number of errors + * @deprecated 3.0.0 This method will be removed in 3.0 + */ + public function validate() + { + $args = func_get_args(); + $errors = call_user_func_array([&$this, 'validateErrors'], $args); + + if ($errors === false) { + return 0; + } + return count($errors); + } + + /** + * Validates models passed by parameters. Takes a list of models as a variable argument. + * Example: + * + * `$errors = $this->validateErrors($this->Article, $this->User);` + * + * @return array|false Validation errors, or false if none + * @deprecated 3.0.0 This method will be removed in 3.0 + */ + public function validateErrors() + { + $objects = func_get_args(); + + if (empty($objects)) { + return false; + } + + $errors = []; + foreach ($objects as $object) { + if (isset($this->{$object->alias})) { + $object = $this->{$object->alias}; + } + $object->set($object->data); + $errors = array_merge($errors, $object->invalidFields()); + } + + return $this->validationErrors = (!empty($errors) ? $errors : false); + } + + /** + * Returns the referring URL for this request. + * + * @param string $default Default URL to use if HTTP_REFERER cannot be read from headers + * @param bool $local If true, restrict referring URLs to local server + * @return string Referring URL + * @link https://book.cakephp.org/2.0/en/controllers.html#Controller::referer + */ + public function referer($default = null, $local = false) + { + if (!$this->request) { + return '/'; + } + + $referer = $this->request->referer($local); + if ($referer === '/' && $default && $default !== $referer) { + return Router::url($default, !$local); + } + return $referer; + } + + /** + * Forces the user's browser not to cache the results of the current request. + * + * @return void + * @link https://book.cakephp.org/2.0/en/controllers.html#Controller::disableCache + * @deprecated 3.0.0 Will be removed in 3.0. Use CakeResponse::disableCache(). + */ + public function disableCache() + { + $this->response->disableCache(); + } + + /** + * Shows a message to the user for $pause seconds, then redirects to $url. + * Uses flash.ctp as the default layout for the message. + * Does not work if the current debug level is higher than 0. + * + * @param string $message Message to display to the user + * @param string|array $url Relative string or array-based URL to redirect to after the time expires + * @param int $pause Time to show the message + * @param string $layout Layout you want to use, defaults to 'flash' + * @return void + * @link https://book.cakephp.org/2.0/en/controllers.html#Controller::flash + * @deprecated 3.0.0 Will be removed in 3.0. Use Flash::set() with version 2.7+ or Session::setFlash() prior to 2.7. + */ + public function flash($message, $url, $pause = 1, $layout = 'flash') + { + $this->autoRender = false; + $this->set('url', Router::url($url)); + $this->set('message', $message); + $this->set('pause', $pause); + $this->set('pageTitle', $message); + $this->render(false, $layout); + } + + /** + * Saves a variable for use inside a view template. + * + * @param string|array $one A string or an array of data. + * @param mixed $two Value in case $one is a string (which then works as the key). + * Unused if $one is an associative array, otherwise serves as the values to $one's keys. + * @return void + * @link https://book.cakephp.org/2.0/en/controllers.html#interacting-with-views + */ + public function set($one, $two = null) + { + if (is_array($one)) { + if (is_array($two)) { + $data = array_combine($one, $two); + } else { + $data = $one; + } + } else { + $data = [$one => $two]; + } + $this->viewVars = $data + $this->viewVars; + } + + /** + * Instantiates the correct view class, hands it its data, and uses it to render the view output. + * + * @param bool|string $view View to use for rendering + * @param string $layout Layout to use + * @return CakeResponse A response object containing the rendered view. + * @triggers Controller.beforeRender $this + * @link https://book.cakephp.org/2.0/en/controllers.html#Controller::render + */ + public function render($view = null, $layout = null) + { + $event = new CakeEvent('Controller.beforeRender', $this); + $this->getEventManager()->dispatch($event); + if ($event->isStopped()) { + $this->autoRender = false; + return $this->response; + } + + if (!empty($this->uses) && is_array($this->uses)) { + foreach ($this->uses as $model) { + list($plugin, $className) = pluginSplit($model); + $this->request->params['models'][$className] = compact('plugin', 'className'); + } + } + + $this->View = $this->_getViewObject(); + + $models = ClassRegistry::keys(); + foreach ($models as $currentModel) { + $currentObject = ClassRegistry::getObject($currentModel); + if ($currentObject instanceof Model) { + $className = get_class($currentObject); + list($plugin) = pluginSplit(App::location($className)); + $this->request->params['models'][$currentObject->alias] = compact('plugin', 'className'); + $this->View->validationErrors[$currentObject->alias] =& $currentObject->validationErrors; + } + } + + $this->autoRender = false; + $this->response->body($this->View->render($view, $layout)); + return $this->response; + } + + /** + * Constructs the view class instance based on the controller property + * + * @return View + */ + protected function _getViewObject() + { + $viewClass = $this->viewClass; + if ($this->viewClass !== 'View') { + list($plugin, $viewClass) = pluginSplit($viewClass, true); + $viewClass = $viewClass . 'View'; + App::uses($viewClass, $plugin . 'View'); + } + + return new $viewClass($this); + } + + /** + * Converts POST'ed form data to a model conditions array. + * + * If combined with SecurityComponent these conditions could be suitable + * for use in a Model::find() call. Without SecurityComponent this method + * is vulnerable creating conditions containing SQL injection. While we + * attempt to raise exceptions. + * + * @param array $data POST'ed data organized by model and field + * @param string|array $op A string containing an SQL comparison operator, or an array matching operators + * to fields + * @param string $bool SQL boolean operator: AND, OR, XOR, etc. + * @param bool $exclusive If true, and $op is an array, fields not included in $op will not be + * included in the returned conditions + * @return array|null An array of model conditions + * @deprecated 3.0.0 Will be removed in 3.0. + * @throws RuntimeException when unsafe operators are found. + */ + public function postConditions($data = [], $op = null, $bool = 'AND', $exclusive = false) + { + if (!is_array($data) || empty($data)) { + if (!empty($this->request->data)) { + $data = $this->request->data; + } else { + return null; + } + } + $cond = []; + + if ($op === null) { + $op = ''; + } + + $allowedChars = '#[^a-zA-Z0-9_ ]#'; + $arrayOp = is_array($op); + foreach ($data as $model => $fields) { + if (preg_match($allowedChars, $model)) { + throw new RuntimeException("Unsafe operator found in {$model}"); + } + foreach ($fields as $field => $value) { + if (preg_match($allowedChars, $field)) { + throw new RuntimeException("Unsafe operator found in {$model}.{$field}"); + } + $key = $model . '.' . $field; + $fieldOp = $op; + if ($arrayOp) { + if (array_key_exists($key, $op)) { + $fieldOp = $op[$key]; + } else if (array_key_exists($field, $op)) { + $fieldOp = $op[$field]; + } else { + $fieldOp = false; + } + } + if ($exclusive && $fieldOp === false) { + continue; + } + $fieldOp = strtoupper(trim($fieldOp)); + if ($fieldOp === 'LIKE') { + $key = $key . ' LIKE'; + $value = '%' . $value . '%'; + } else if ($fieldOp && $fieldOp !== '=') { + $key = $key . ' ' . $fieldOp; + } + $cond[$key] = $value; + } + } + if ($bool && strtoupper($bool) !== 'AND') { + $cond = [$bool => $cond]; + } + return $cond; + } + + /** + * Handles automatic pagination of model records. + * + * @param Model|string $object Model to paginate (e.g: model instance, or 'Model', or 'Model.InnerModel') + * @param string|array $scope Conditions to use while paginating + * @param array $whitelist List of allowed options for paging + * @return array Model query results + * @link https://book.cakephp.org/2.0/en/controllers.html#Controller::paginate + */ + public function paginate($object = null, $scope = [], $whitelist = []) + { + return $this->Components->load('Paginator', $this->paginate)->paginate($object, $scope, $whitelist); + } + + /** + * Called before the controller action. You can use this method to configure and customize components + * or perform logic that needs to happen before each controller action. + * + * @return void + * @link https://book.cakephp.org/2.0/en/controllers.html#request-life-cycle-callbacks + */ + public function beforeFilter() + { + } + + /** + * Called after the controller action is run, but before the view is rendered. You can use this method + * to perform logic or set view variables that are required on every request. + * + * @return void + * @link https://book.cakephp.org/2.0/en/controllers.html#request-life-cycle-callbacks + */ + public function beforeRender() + { + } + + /** + * The beforeRedirect method is invoked when the controller's redirect method is called but before any + * further action. + * + * If this method returns false the controller will not continue on to redirect the request. + * The $url, $status and $exit variables have same meaning as for the controller's method. You can also + * return a string which will be interpreted as the URL to redirect to or return associative array with + * key 'url' and optionally 'status' and 'exit'. + * + * @param string|array $url A string or array-based URL pointing to another location within the app, + * or an absolute URL + * @param int $status Optional HTTP status code (eg: 404) + * @param bool $exit If true, exit() will be called after the redirect + * @return mixed + * false to stop redirection event, + * string controllers a new redirection URL or + * array with the keys url, status and exit to be used by the redirect method. + * @link https://book.cakephp.org/2.0/en/controllers.html#request-life-cycle-callbacks + */ + public function beforeRedirect($url, $status = null, $exit = true) + { + } + + /** + * Called after the controller action is run and rendered. + * + * @return void + * @link https://book.cakephp.org/2.0/en/controllers.html#request-life-cycle-callbacks + */ + public function afterFilter() + { + } + + /** + * Alias to beforeScaffold() + * + * @param string $method Method name. + * @return bool + * @see Controller::beforeScaffold() + * @deprecated 3.0.0 Will be removed in 3.0. + */ + protected function _beforeScaffold($method) + { + return $this->beforeScaffold($method); + } + + /** + * This method should be overridden in child classes. + * + * @param string $method name of method called example index, edit, etc. + * @return bool Success + * @link https://book.cakephp.org/2.0/en/controllers.html#callbacks + */ + public function beforeScaffold($method) + { + return true; + } + + /** + * Alias to afterScaffoldSave() + * + * @param string $method Method name. + * @return bool + * @see Controller::afterScaffoldSave() + * @deprecated 3.0.0 Will be removed in 3.0. + */ + protected function _afterScaffoldSave($method) + { + return $this->afterScaffoldSave($method); + } + + /** + * This method should be overridden in child classes. + * + * @param string $method name of method called either edit or update. + * @return bool Success + * @link https://book.cakephp.org/2.0/en/controllers.html#callbacks + */ + public function afterScaffoldSave($method) + { + return true; + } + + /** + * Alias to afterScaffoldSaveError() + * + * @param string $method Method name. + * @return bool + * @see Controller::afterScaffoldSaveError() + * @deprecated 3.0.0 Will be removed in 3.0. + */ + protected function _afterScaffoldSaveError($method) + { + return $this->afterScaffoldSaveError($method); + } + + /** + * This method should be overridden in child classes. + * + * @param string $method name of method called either edit or update. + * @return bool Success + * @link https://book.cakephp.org/2.0/en/controllers.html#callbacks + */ + public function afterScaffoldSaveError($method) + { + return true; + } + + /** + * Alias to scaffoldError() + * + * @param string $method Method name. + * @return bool + * @see Controller::scaffoldError() + * @deprecated 3.0.0 Will be removed in 3.0. + */ + protected function _scaffoldError($method) + { + return $this->scaffoldError($method); + } + + /** + * This method should be overridden in child classes. + * If not it will render a scaffold error. + * Method MUST return true in child classes + * + * @param string $method name of method called example index, edit, etc. + * @return bool Success + * @link https://book.cakephp.org/2.0/en/controllers.html#callbacks + */ + public function scaffoldError($method) + { + return false; + } } diff --git a/lib/Cake/Controller/Scaffold.php b/lib/Cake/Controller/Scaffold.php index 33999ecb..4c7c5521 100755 --- a/lib/Cake/Controller/Scaffold.php +++ b/lib/Cake/Controller/Scaffold.php @@ -29,423 +29,431 @@ * @package Cake.Controller * @deprecated 3.0.0 Dynamic scaffolding will be removed and replaced in 3.0 */ -class Scaffold { - -/** - * Controller object - * - * @var Controller - */ - public $controller = null; - -/** - * Name of the controller to scaffold - * - * @var string - */ - public $name = null; - -/** - * Name of current model this view context is attached to - * - * @var string - */ - public $model = null; - -/** - * Path to View. - * - * @var string - */ - public $viewPath; - -/** - * Name of layout to use with this View. - * - * @var string - */ - public $layout = 'default'; - -/** - * Request object - * - * @var CakeRequest - */ - public $request; - -/** - * Valid session. - * - * @var bool - */ - protected $_validSession = null; - -/** - * List of variables to collect from the associated controller - * - * @var array - */ - protected $_passedVars = array( - 'layout', 'name', 'viewPath', 'request' - ); - -/** - * Title HTML element for current scaffolded view - * - * @var string - */ - public $scaffoldTitle = null; - -/** - * Construct and set up given controller with given parameters. - * - * @param Controller $controller Controller to scaffold - * @param CakeRequest $request Request parameters. - * @throws MissingModelException - */ - public function __construct(Controller $controller, CakeRequest $request) { - $this->controller = $controller; - - $count = count($this->_passedVars); - for ($j = 0; $j < $count; $j++) { - $var = $this->_passedVars[$j]; - $this->{$var} = $controller->{$var}; - } - - $this->redirect = array('action' => 'index'); - - $this->modelClass = $controller->modelClass; - $this->modelKey = $controller->modelKey; - - if (!is_object($this->controller->{$this->modelClass})) { - throw new MissingModelException($this->modelClass); - } - - $this->ScaffoldModel = $this->controller->{$this->modelClass}; - $this->scaffoldTitle = Inflector::humanize(Inflector::underscore($this->viewPath)); - $this->scaffoldActions = $controller->scaffold; - $title = __d('cake', 'Scaffold :: ') . Inflector::humanize($request->action) . ' :: ' . $this->scaffoldTitle; - $modelClass = $this->controller->modelClass; - $primaryKey = $this->ScaffoldModel->primaryKey; - $displayField = $this->ScaffoldModel->displayField; - $singularVar = Inflector::variable($modelClass); - $pluralVar = Inflector::variable($this->controller->name); - $singularHumanName = Inflector::humanize(Inflector::underscore($modelClass)); - $pluralHumanName = Inflector::humanize(Inflector::underscore($this->controller->name)); - $scaffoldFields = array_keys($this->ScaffoldModel->schema()); - $associations = $this->_associations(); - - $this->controller->set(compact( - 'modelClass', 'primaryKey', 'displayField', 'singularVar', 'pluralVar', - 'singularHumanName', 'pluralHumanName', 'scaffoldFields', 'associations' - )); - $this->controller->set('title_for_layout', $title); - - if ($this->controller->viewClass) { - $this->controller->viewClass = 'Scaffold'; - } - $this->_validSession = ( - isset($this->controller->Session) && - $this->controller->Session->valid() && - isset($this->controller->Flash) - ); - $this->_scaffold($request); - } - -/** - * Renders a view action of scaffolded model. - * - * @param CakeRequest $request Request Object for scaffolding - * @return mixed A rendered view of a row from Models database table - * @throws NotFoundException - */ - protected function _scaffoldView(CakeRequest $request) { - if ($this->controller->beforeScaffold('view')) { - if (isset($request->params['pass'][0])) { - $this->ScaffoldModel->id = $request->params['pass'][0]; - } - if (!$this->ScaffoldModel->exists()) { - throw new NotFoundException(__d('cake', 'Invalid %s', Inflector::humanize($this->modelKey))); - } - $this->ScaffoldModel->recursive = 1; - $this->controller->request->data = $this->ScaffoldModel->read(); - $this->controller->set( - Inflector::variable($this->controller->modelClass), $this->request->data - ); - $this->controller->render($this->request['action'], $this->layout); - } elseif ($this->controller->scaffoldError('view') === false) { - return $this->_scaffoldError(); - } - } - -/** - * Renders index action of scaffolded model. - * - * @param array $params Parameters for scaffolding - * @return mixed A rendered view listing rows from Models database table - */ - protected function _scaffoldIndex($params) { - if ($this->controller->beforeScaffold('index')) { - $this->ScaffoldModel->recursive = 0; - $this->controller->set( - Inflector::variable($this->controller->name), $this->controller->paginate() - ); - $this->controller->render($this->request['action'], $this->layout); - } elseif ($this->controller->scaffoldError('index') === false) { - return $this->_scaffoldError(); - } - } - -/** - * Renders an add or edit action for scaffolded model. - * - * @param string $action Action (add or edit) - * @return void - */ - protected function _scaffoldForm($action = 'edit') { - $this->controller->viewVars['scaffoldFields'] = array_merge( - $this->controller->viewVars['scaffoldFields'], - array_keys($this->ScaffoldModel->hasAndBelongsToMany) - ); - $this->controller->render($action, $this->layout); - } - -/** - * Saves or updates the scaffolded model. - * - * @param CakeRequest $request Request Object for scaffolding - * @param string $action add or edit - * @return mixed Success on save/update, add/edit form if data is empty or error if save or update fails - * @throws NotFoundException - */ - protected function _scaffoldSave(CakeRequest $request, $action = 'edit') { - $formAction = 'edit'; - $success = __d('cake', 'updated'); - if ($action === 'add') { - $formAction = 'add'; - $success = __d('cake', 'saved'); - } - - if ($this->controller->beforeScaffold($action)) { - if ($action === 'edit') { - if (isset($request->params['pass'][0])) { - $this->ScaffoldModel->id = $request['pass'][0]; - } - if (!$this->ScaffoldModel->exists()) { - throw new NotFoundException(__d('cake', 'Invalid %s', Inflector::humanize($this->modelKey))); - } - } - - if (!empty($request->data)) { - if ($action === 'create') { - $this->ScaffoldModel->create(); - } - - if ($this->ScaffoldModel->save($request->data)) { - if ($this->controller->afterScaffoldSave($action)) { - $message = __d('cake', - 'The %1$s has been %2$s', - Inflector::humanize($this->modelKey), - $success - ); - return $this->_sendMessage($message, 'success'); - } - return $this->controller->afterScaffoldSaveError($action); - } - if ($this->_validSession) { - $this->controller->Flash->set(__d('cake', 'Please correct errors below.')); - } - } - - if (empty($request->data)) { - if ($this->ScaffoldModel->id) { - $this->controller->data = $request->data = $this->ScaffoldModel->read(); - } else { - $this->controller->data = $request->data = $this->ScaffoldModel->create(); - } - } - - foreach ($this->ScaffoldModel->belongsTo as $assocName => $assocData) { - $varName = Inflector::variable(Inflector::pluralize( - preg_replace('/(?:_id)$/', '', $assocData['foreignKey']) - )); - $this->controller->set($varName, $this->ScaffoldModel->{$assocName}->find('list')); - } - foreach ($this->ScaffoldModel->hasAndBelongsToMany as $assocName => $assocData) { - $varName = Inflector::variable(Inflector::pluralize($assocName)); - $this->controller->set($varName, $this->ScaffoldModel->{$assocName}->find('list')); - } - - return $this->_scaffoldForm($formAction); - } elseif ($this->controller->scaffoldError($action) === false) { - return $this->_scaffoldError(); - } - } - -/** - * Performs a delete on given scaffolded Model. - * - * @param CakeRequest $request Request for scaffolding - * @return mixed Success on delete, error if delete fails - * @throws MethodNotAllowedException When HTTP method is not a DELETE - * @throws NotFoundException When id being deleted does not exist. - */ - protected function _scaffoldDelete(CakeRequest $request) { - if ($this->controller->beforeScaffold('delete')) { - if (!$request->is('post')) { - throw new MethodNotAllowedException(); - } - $id = false; - if (isset($request->params['pass'][0])) { - $id = $request->params['pass'][0]; - } - $this->ScaffoldModel->id = $id; - if (!$this->ScaffoldModel->exists()) { - throw new NotFoundException(__d('cake', 'Invalid %s', Inflector::humanize($this->modelClass))); - } - if ($this->ScaffoldModel->delete()) { - $message = __d('cake', 'The %1$s with id: %2$s has been deleted.', Inflector::humanize($this->modelClass), $id); - return $this->_sendMessage($message, 'success'); - } - $message = __d('cake', - 'There was an error deleting the %1$s with id: %2$s', - Inflector::humanize($this->modelClass), - $id - ); - return $this->_sendMessage($message); - } elseif ($this->controller->scaffoldError('delete') === false) { - return $this->_scaffoldError(); - } - } - -/** - * Sends a message to the user. Either uses Sessions or flash messages depending - * on the availability of a session - * - * @param string $message Message to display - * @param string $element Flash template to use - * @return CakeResponse|null - */ - protected function _sendMessage($message, $element = 'default') { - if ($this->_validSession) { - $this->controller->Flash->set($message, compact('element')); - return $this->controller->redirect($this->redirect); - } - $this->controller->flash($message, $this->redirect); - } - -/** - * Show a scaffold error - * - * @return mixed A rendered view showing the error - */ - protected function _scaffoldError() { - return $this->controller->render('error', $this->layout); - } - -/** - * When methods are now present in a controller - * scaffoldView is used to call default Scaffold methods if: - * `public $scaffold;` is placed in the controller's class definition. - * - * @param CakeRequest $request Request object for scaffolding - * @return void - * @throws MissingActionException When methods are not scaffolded. - * @throws MissingDatabaseException When the database connection is undefined. - */ - protected function _scaffold(CakeRequest $request) { - $db = ConnectionManager::getDataSource($this->ScaffoldModel->useDbConfig); - $prefixes = Configure::read('Routing.prefixes'); - $scaffoldPrefix = $this->scaffoldActions; - - if (isset($db)) { - if (empty($this->scaffoldActions)) { - $this->scaffoldActions = array( - 'index', 'list', 'view', 'add', 'create', 'edit', 'update', 'delete' - ); - } elseif (!empty($prefixes) && in_array($scaffoldPrefix, $prefixes)) { - $this->scaffoldActions = array( - $scaffoldPrefix . '_index', - $scaffoldPrefix . '_list', - $scaffoldPrefix . '_view', - $scaffoldPrefix . '_add', - $scaffoldPrefix . '_create', - $scaffoldPrefix . '_edit', - $scaffoldPrefix . '_update', - $scaffoldPrefix . '_delete' - ); - } - - if (in_array($request->params['action'], $this->scaffoldActions)) { - if (!empty($prefixes)) { - $request->params['action'] = str_replace($scaffoldPrefix . '_', '', $request->params['action']); - } - switch ($request->params['action']) { - case 'index': - case 'list': - $this->_scaffoldIndex($request); - break; - case 'view': - $this->_scaffoldView($request); - break; - case 'add': - case 'create': - $this->_scaffoldSave($request, 'add'); - break; - case 'edit': - case 'update': - $this->_scaffoldSave($request, 'edit'); - break; - case 'delete': - $this->_scaffoldDelete($request); - break; - } - } else { - throw new MissingActionException(array( - 'controller' => get_class($this->controller), - 'action' => $request->action - )); - } - } else { - throw new MissingDatabaseException(array('connection' => $this->ScaffoldModel->useDbConfig)); - } - } - -/** - * Returns associations for controllers models. - * - * @return array Associations for model - */ - protected function _associations() { - $keys = array('belongsTo', 'hasOne', 'hasMany', 'hasAndBelongsToMany'); - $associations = array(); - - foreach ($keys as $type) { - foreach ($this->ScaffoldModel->{$type} as $assocKey => $assocData) { - $associations[$type][$assocKey]['primaryKey'] = - $this->ScaffoldModel->{$assocKey}->primaryKey; - - $associations[$type][$assocKey]['displayField'] = - $this->ScaffoldModel->{$assocKey}->displayField; - - $associations[$type][$assocKey]['foreignKey'] = - $assocData['foreignKey']; - - list($plugin, $model) = pluginSplit($assocData['className']); - if ($plugin) { - $plugin = Inflector::underscore($plugin); - } - $associations[$type][$assocKey]['plugin'] = $plugin; - - $associations[$type][$assocKey]['controller'] = - Inflector::pluralize(Inflector::underscore($model)); - - if ($type === 'hasAndBelongsToMany') { - $associations[$type][$assocKey]['with'] = $assocData['with']; - } - } - } - return $associations; - } +class Scaffold +{ + + /** + * Controller object + * + * @var Controller + */ + public $controller = null; + + /** + * Name of the controller to scaffold + * + * @var string + */ + public $name = null; + + /** + * Name of current model this view context is attached to + * + * @var string + */ + public $model = null; + + /** + * Path to View. + * + * @var string + */ + public $viewPath; + + /** + * Name of layout to use with this View. + * + * @var string + */ + public $layout = 'default'; + + /** + * Request object + * + * @var CakeRequest + */ + public $request; + /** + * Title HTML element for current scaffolded view + * + * @var string + */ + public $scaffoldTitle = null; + /** + * Valid session. + * + * @var bool + */ + protected $_validSession = null; + /** + * List of variables to collect from the associated controller + * + * @var array + */ + protected $_passedVars = [ + 'layout', 'name', 'viewPath', 'request' + ]; + + /** + * Construct and set up given controller with given parameters. + * + * @param Controller $controller Controller to scaffold + * @param CakeRequest $request Request parameters. + * @throws MissingModelException + */ + public function __construct(Controller $controller, CakeRequest $request) + { + $this->controller = $controller; + + $count = count($this->_passedVars); + for ($j = 0; $j < $count; $j++) { + $var = $this->_passedVars[$j]; + $this->{$var} = $controller->{$var}; + } + + $this->redirect = ['action' => 'index']; + + $this->modelClass = $controller->modelClass; + $this->modelKey = $controller->modelKey; + + if (!is_object($this->controller->{$this->modelClass})) { + throw new MissingModelException($this->modelClass); + } + + $this->ScaffoldModel = $this->controller->{$this->modelClass}; + $this->scaffoldTitle = Inflector::humanize(Inflector::underscore($this->viewPath)); + $this->scaffoldActions = $controller->scaffold; + $title = __d('cake', 'Scaffold :: ') . Inflector::humanize($request->action) . ' :: ' . $this->scaffoldTitle; + $modelClass = $this->controller->modelClass; + $primaryKey = $this->ScaffoldModel->primaryKey; + $displayField = $this->ScaffoldModel->displayField; + $singularVar = Inflector::variable($modelClass); + $pluralVar = Inflector::variable($this->controller->name); + $singularHumanName = Inflector::humanize(Inflector::underscore($modelClass)); + $pluralHumanName = Inflector::humanize(Inflector::underscore($this->controller->name)); + $scaffoldFields = array_keys($this->ScaffoldModel->schema()); + $associations = $this->_associations(); + + $this->controller->set(compact( + 'modelClass', 'primaryKey', 'displayField', 'singularVar', 'pluralVar', + 'singularHumanName', 'pluralHumanName', 'scaffoldFields', 'associations' + )); + $this->controller->set('title_for_layout', $title); + + if ($this->controller->viewClass) { + $this->controller->viewClass = 'Scaffold'; + } + $this->_validSession = ( + isset($this->controller->Session) && + $this->controller->Session->valid() && + isset($this->controller->Flash) + ); + $this->_scaffold($request); + } + + /** + * Returns associations for controllers models. + * + * @return array Associations for model + */ + protected function _associations() + { + $keys = ['belongsTo', 'hasOne', 'hasMany', 'hasAndBelongsToMany']; + $associations = []; + + foreach ($keys as $type) { + foreach ($this->ScaffoldModel->{$type} as $assocKey => $assocData) { + $associations[$type][$assocKey]['primaryKey'] = + $this->ScaffoldModel->{$assocKey}->primaryKey; + + $associations[$type][$assocKey]['displayField'] = + $this->ScaffoldModel->{$assocKey}->displayField; + + $associations[$type][$assocKey]['foreignKey'] = + $assocData['foreignKey']; + + list($plugin, $model) = pluginSplit($assocData['className']); + if ($plugin) { + $plugin = Inflector::underscore($plugin); + } + $associations[$type][$assocKey]['plugin'] = $plugin; + + $associations[$type][$assocKey]['controller'] = + Inflector::pluralize(Inflector::underscore($model)); + + if ($type === 'hasAndBelongsToMany') { + $associations[$type][$assocKey]['with'] = $assocData['with']; + } + } + } + return $associations; + } + + /** + * When methods are now present in a controller + * scaffoldView is used to call default Scaffold methods if: + * `public $scaffold;` is placed in the controller's class definition. + * + * @param CakeRequest $request Request object for scaffolding + * @return void + * @throws MissingActionException When methods are not scaffolded. + * @throws MissingDatabaseException When the database connection is undefined. + */ + protected function _scaffold(CakeRequest $request) + { + $db = ConnectionManager::getDataSource($this->ScaffoldModel->useDbConfig); + $prefixes = Configure::read('Routing.prefixes'); + $scaffoldPrefix = $this->scaffoldActions; + + if (isset($db)) { + if (empty($this->scaffoldActions)) { + $this->scaffoldActions = [ + 'index', 'list', 'view', 'add', 'create', 'edit', 'update', 'delete' + ]; + } else if (!empty($prefixes) && in_array($scaffoldPrefix, $prefixes)) { + $this->scaffoldActions = [ + $scaffoldPrefix . '_index', + $scaffoldPrefix . '_list', + $scaffoldPrefix . '_view', + $scaffoldPrefix . '_add', + $scaffoldPrefix . '_create', + $scaffoldPrefix . '_edit', + $scaffoldPrefix . '_update', + $scaffoldPrefix . '_delete' + ]; + } + + if (in_array($request->params['action'], $this->scaffoldActions)) { + if (!empty($prefixes)) { + $request->params['action'] = str_replace($scaffoldPrefix . '_', '', $request->params['action']); + } + switch ($request->params['action']) { + case 'index': + case 'list': + $this->_scaffoldIndex($request); + break; + case 'view': + $this->_scaffoldView($request); + break; + case 'add': + case 'create': + $this->_scaffoldSave($request, 'add'); + break; + case 'edit': + case 'update': + $this->_scaffoldSave($request, 'edit'); + break; + case 'delete': + $this->_scaffoldDelete($request); + break; + } + } else { + throw new MissingActionException([ + 'controller' => get_class($this->controller), + 'action' => $request->action + ]); + } + } else { + throw new MissingDatabaseException(['connection' => $this->ScaffoldModel->useDbConfig]); + } + } + + /** + * Renders index action of scaffolded model. + * + * @param array $params Parameters for scaffolding + * @return mixed A rendered view listing rows from Models database table + */ + protected function _scaffoldIndex($params) + { + if ($this->controller->beforeScaffold('index')) { + $this->ScaffoldModel->recursive = 0; + $this->controller->set( + Inflector::variable($this->controller->name), $this->controller->paginate() + ); + $this->controller->render($this->request['action'], $this->layout); + } else if ($this->controller->scaffoldError('index') === false) { + return $this->_scaffoldError(); + } + } + + /** + * Show a scaffold error + * + * @return mixed A rendered view showing the error + */ + protected function _scaffoldError() + { + return $this->controller->render('error', $this->layout); + } + + /** + * Renders a view action of scaffolded model. + * + * @param CakeRequest $request Request Object for scaffolding + * @return mixed A rendered view of a row from Models database table + * @throws NotFoundException + */ + protected function _scaffoldView(CakeRequest $request) + { + if ($this->controller->beforeScaffold('view')) { + if (isset($request->params['pass'][0])) { + $this->ScaffoldModel->id = $request->params['pass'][0]; + } + if (!$this->ScaffoldModel->exists()) { + throw new NotFoundException(__d('cake', 'Invalid %s', Inflector::humanize($this->modelKey))); + } + $this->ScaffoldModel->recursive = 1; + $this->controller->request->data = $this->ScaffoldModel->read(); + $this->controller->set( + Inflector::variable($this->controller->modelClass), $this->request->data + ); + $this->controller->render($this->request['action'], $this->layout); + } else if ($this->controller->scaffoldError('view') === false) { + return $this->_scaffoldError(); + } + } + + /** + * Saves or updates the scaffolded model. + * + * @param CakeRequest $request Request Object for scaffolding + * @param string $action add or edit + * @return mixed Success on save/update, add/edit form if data is empty or error if save or update fails + * @throws NotFoundException + */ + protected function _scaffoldSave(CakeRequest $request, $action = 'edit') + { + $formAction = 'edit'; + $success = __d('cake', 'updated'); + if ($action === 'add') { + $formAction = 'add'; + $success = __d('cake', 'saved'); + } + + if ($this->controller->beforeScaffold($action)) { + if ($action === 'edit') { + if (isset($request->params['pass'][0])) { + $this->ScaffoldModel->id = $request['pass'][0]; + } + if (!$this->ScaffoldModel->exists()) { + throw new NotFoundException(__d('cake', 'Invalid %s', Inflector::humanize($this->modelKey))); + } + } + + if (!empty($request->data)) { + if ($action === 'create') { + $this->ScaffoldModel->create(); + } + + if ($this->ScaffoldModel->save($request->data)) { + if ($this->controller->afterScaffoldSave($action)) { + $message = __d('cake', + 'The %1$s has been %2$s', + Inflector::humanize($this->modelKey), + $success + ); + return $this->_sendMessage($message, 'success'); + } + return $this->controller->afterScaffoldSaveError($action); + } + if ($this->_validSession) { + $this->controller->Flash->set(__d('cake', 'Please correct errors below.')); + } + } + + if (empty($request->data)) { + if ($this->ScaffoldModel->id) { + $this->controller->data = $request->data = $this->ScaffoldModel->read(); + } else { + $this->controller->data = $request->data = $this->ScaffoldModel->create(); + } + } + + foreach ($this->ScaffoldModel->belongsTo as $assocName => $assocData) { + $varName = Inflector::variable(Inflector::pluralize( + preg_replace('/(?:_id)$/', '', $assocData['foreignKey']) + )); + $this->controller->set($varName, $this->ScaffoldModel->{$assocName}->find('list')); + } + foreach ($this->ScaffoldModel->hasAndBelongsToMany as $assocName => $assocData) { + $varName = Inflector::variable(Inflector::pluralize($assocName)); + $this->controller->set($varName, $this->ScaffoldModel->{$assocName}->find('list')); + } + + return $this->_scaffoldForm($formAction); + } else if ($this->controller->scaffoldError($action) === false) { + return $this->_scaffoldError(); + } + } + + /** + * Sends a message to the user. Either uses Sessions or flash messages depending + * on the availability of a session + * + * @param string $message Message to display + * @param string $element Flash template to use + * @return CakeResponse|null + */ + protected function _sendMessage($message, $element = 'default') + { + if ($this->_validSession) { + $this->controller->Flash->set($message, compact('element')); + return $this->controller->redirect($this->redirect); + } + $this->controller->flash($message, $this->redirect); + } + + /** + * Renders an add or edit action for scaffolded model. + * + * @param string $action Action (add or edit) + * @return void + */ + protected function _scaffoldForm($action = 'edit') + { + $this->controller->viewVars['scaffoldFields'] = array_merge( + $this->controller->viewVars['scaffoldFields'], + array_keys($this->ScaffoldModel->hasAndBelongsToMany) + ); + $this->controller->render($action, $this->layout); + } + + /** + * Performs a delete on given scaffolded Model. + * + * @param CakeRequest $request Request for scaffolding + * @return mixed Success on delete, error if delete fails + * @throws MethodNotAllowedException When HTTP method is not a DELETE + * @throws NotFoundException When id being deleted does not exist. + */ + protected function _scaffoldDelete(CakeRequest $request) + { + if ($this->controller->beforeScaffold('delete')) { + if (!$request->is('post')) { + throw new MethodNotAllowedException(); + } + $id = false; + if (isset($request->params['pass'][0])) { + $id = $request->params['pass'][0]; + } + $this->ScaffoldModel->id = $id; + if (!$this->ScaffoldModel->exists()) { + throw new NotFoundException(__d('cake', 'Invalid %s', Inflector::humanize($this->modelClass))); + } + if ($this->ScaffoldModel->delete()) { + $message = __d('cake', 'The %1$s with id: %2$s has been deleted.', Inflector::humanize($this->modelClass), $id); + return $this->_sendMessage($message, 'success'); + } + $message = __d('cake', + 'There was an error deleting the %1$s with id: %2$s', + Inflector::humanize($this->modelClass), + $id + ); + return $this->_sendMessage($message); + } else if ($this->controller->scaffoldError('delete') === false) { + return $this->_scaffoldError(); + } + } } diff --git a/lib/Cake/Core/App.php b/lib/Cake/Core/App.php index 5aeac302..15d7a660 100755 --- a/lib/Cake/Core/App.php +++ b/lib/Cake/Core/App.php @@ -38,7 +38,7 @@ * * For instance if you'd like to use your own HttpSocket class, put it under * - * app/Network/Http/HttpSocket.php + * app/Network/Http/HttpSocket.php * * ### Inspecting loaded paths * @@ -62,912 +62,925 @@ * @link https://book.cakephp.org/2.0/en/core-utility-libraries/app.html * @package Cake.Core */ -class App { - -/** - * Append paths - * - * @var string - */ - const APPEND = 'append'; - -/** - * Prepend paths - * - * @var string - */ - const PREPEND = 'prepend'; - -/** - * Register package - * - * @var string - */ - const REGISTER = 'register'; - -/** - * Reset paths instead of merging - * - * @var bool - */ - const RESET = true; - -/** - * List of object types and their properties - * - * @var array - */ - public static $types = array( - 'class' => array('extends' => null, 'core' => true), - 'file' => array('extends' => null, 'core' => true), - 'model' => array('extends' => 'AppModel', 'core' => false), - 'behavior' => array('suffix' => 'Behavior', 'extends' => 'Model/ModelBehavior', 'core' => true), - 'controller' => array('suffix' => 'Controller', 'extends' => 'AppController', 'core' => true), - 'component' => array('suffix' => 'Component', 'extends' => null, 'core' => true), - 'lib' => array('extends' => null, 'core' => true), - 'view' => array('suffix' => 'View', 'extends' => null, 'core' => true), - 'helper' => array('suffix' => 'Helper', 'extends' => 'AppHelper', 'core' => true), - 'vendor' => array('extends' => null, 'core' => true), - 'shell' => array('suffix' => 'Shell', 'extends' => 'AppShell', 'core' => true), - 'plugin' => array('extends' => null, 'core' => true) - ); - -/** - * Paths to search for files. - * - * @var array - */ - public static $search = array(); - -/** - * Whether or not to return the file that is loaded. - * - * @var bool - */ - public static $return = false; - -/** - * Holds key/value pairs of $type => file path. - * - * @var array - */ - protected static $_map = array(); - -/** - * Holds and key => value array of object types. - * - * @var array - */ - protected static $_objects = array(); - -/** - * Holds the location of each class - * - * @var array - */ - protected static $_classMap = array(); - -/** - * Holds the possible paths for each package name - * - * @var array - */ - protected static $_packages = array(); - -/** - * Holds the templates for each customizable package path in the application - * - * @var array - */ - protected static $_packageFormat = array(); - -/** - * Maps an old style CakePHP class type to the corresponding package - * - * @var array - */ - public static $legacy = array( - 'models' => 'Model', - 'behaviors' => 'Model/Behavior', - 'datasources' => 'Model/Datasource', - 'controllers' => 'Controller', - 'components' => 'Controller/Component', - 'views' => 'View', - 'helpers' => 'View/Helper', - 'shells' => 'Console/Command', - 'libs' => 'Lib', - 'vendors' => 'Vendor', - 'plugins' => 'Plugin', - 'locales' => 'Locale' - ); - -/** - * Indicates whether the class cache should be stored again because of an addition to it - * - * @var bool - */ - protected static $_cacheChange = false; - -/** - * Indicates whether the object cache should be stored again because of an addition to it - * - * @var bool - */ - protected static $_objectCacheChange = false; - -/** - * Indicates the the Application is in the bootstrapping process. Used to better cache - * loaded classes while the cache libraries have not been yet initialized - * - * @var bool - */ - public static $bootstrapping = false; - -/** - * Used to read information stored path - * - * Usage: - * - * `App::path('Model'); will return all paths for models` - * - * `App::path('Model/Datasource', 'MyPlugin'); will return the path for datasources under the 'MyPlugin' plugin` - * - * @param string $type type of path - * @param string $plugin name of plugin - * @return array - * @link https://book.cakephp.org/2.0/en/core-utility-libraries/app.html#App::path - */ - public static function path($type, $plugin = null) { - if (!empty(static::$legacy[$type])) { - $type = static::$legacy[$type]; - } - - if (!empty($plugin)) { - $path = array(); - $pluginPath = CakePlugin::path($plugin); - $packageFormat = static::_packageFormat(); - if (!empty($packageFormat[$type])) { - foreach ($packageFormat[$type] as $f) { - $path[] = sprintf($f, $pluginPath); - } - } - return $path; - } - - if (!isset(static::$_packages[$type])) { - return array(); - } - return static::$_packages[$type]; - } - -/** - * Get all the currently loaded paths from App. Useful for inspecting - * or storing all paths App knows about. For a paths to a specific package - * use App::path() - * - * @return array An array of packages and their associated paths. - * @link https://book.cakephp.org/2.0/en/core-utility-libraries/app.html#App::paths - */ - public static function paths() { - return static::$_packages; - } - -/** - * Sets up each package location on the file system. You can configure multiple search paths - * for each package, those will be used to look for files one folder at a time in the specified order - * All paths should be terminated with a Directory separator - * - * Usage: - * - * `App::build(array('Model' => array('/a/full/path/to/models/'))); will setup a new search path for the Model package` - * - * `App::build(array('Model' => array('/path/to/models/')), App::RESET); will setup the path as the only valid path for searching models` - * - * `App::build(array('View/Helper' => array('/path/to/helpers/', '/another/path/'))); will setup multiple search paths for helpers` - * - * `App::build(array('Service' => array('%s' . 'Service' . DS)), App::REGISTER); will register new package 'Service'` - * - * If reset is set to true, all loaded plugins will be forgotten and they will be needed to be loaded again. - * - * @param array $paths associative array with package names as keys and a list of directories for new search paths - * @param bool|string $mode App::RESET will set paths, App::APPEND with append paths, App::PREPEND will prepend paths (default) - * App::REGISTER will register new packages and their paths, %s in path will be replaced by APP path - * @return void - * @link https://book.cakephp.org/2.0/en/core-utility-libraries/app.html#App::build - */ - public static function build($paths = array(), $mode = App::PREPEND) { - //Provides Backwards compatibility for old-style package names - $legacyPaths = array(); - foreach ($paths as $type => $path) { - if (!empty(static::$legacy[$type])) { - $type = static::$legacy[$type]; - } - $legacyPaths[$type] = $path; - } - $paths = $legacyPaths; - - if ($mode === App::RESET) { - foreach ($paths as $type => $new) { - static::$_packages[$type] = (array)$new; - static::objects($type, null, false); - } - return; - } - - if (empty($paths)) { - static::$_packageFormat = null; - } - - $packageFormat = static::_packageFormat(); - - if ($mode === App::REGISTER) { - foreach ($paths as $package => $formats) { - if (empty($packageFormat[$package])) { - $packageFormat[$package] = $formats; - } else { - $formats = array_merge($packageFormat[$package], $formats); - $packageFormat[$package] = array_values(array_unique($formats)); - } - } - static::$_packageFormat = $packageFormat; - } - - $defaults = array(); - foreach ($packageFormat as $package => $format) { - foreach ($format as $f) { - $defaults[$package][] = sprintf($f, APP); - } - } - - if (empty($paths)) { - static::$_packages = $defaults; - return; - } - - if ($mode === App::REGISTER) { - $paths = array(); - } - - foreach ($defaults as $type => $default) { - if (!empty(static::$_packages[$type])) { - $path = static::$_packages[$type]; - } else { - $path = $default; - } - - if (!empty($paths[$type])) { - $newPath = (array)$paths[$type]; - - if ($mode === App::PREPEND) { - $path = array_merge($newPath, $path); - } else { - $path = array_merge($path, $newPath); - } - - $path = array_values(array_unique($path)); - } - - static::$_packages[$type] = $path; - } - } - -/** - * Gets the path that a plugin is on. Searches through the defined plugin paths. - * - * Usage: - * - * `App::pluginPath('MyPlugin'); will return the full path to 'MyPlugin' plugin'` - * - * @param string $plugin CamelCased/lower_cased plugin name to find the path of. - * @return string full path to the plugin. - * @link https://book.cakephp.org/2.0/en/core-utility-libraries/app.html#App::pluginPath - * @deprecated 3.0.0 Use `CakePlugin::path()` instead. - */ - public static function pluginPath($plugin) { - return CakePlugin::path($plugin); - } - -/** - * Finds the path that a theme is on. Searches through the defined theme paths. - * - * Usage: - * - * `App::themePath('MyTheme'); will return the full path to the 'MyTheme' theme` - * - * @param string $theme theme name to find the path of. - * @return string full path to the theme. - * @link https://book.cakephp.org/2.0/en/core-utility-libraries/app.html#App::themePath - */ - public static function themePath($theme) { - $themeDir = 'Themed' . DS . Inflector::camelize($theme); - foreach (static::$_packages['View'] as $path) { - if (is_dir($path . $themeDir)) { - return $path . $themeDir . DS; - } - } - return static::$_packages['View'][0] . $themeDir . DS; - } - -/** - * Returns the full path to a package inside the CakePHP core - * - * Usage: - * - * `App::core('Cache/Engine'); will return the full path to the cache engines package` - * - * @param string $type Package type. - * @return array full path to package - * @link https://book.cakephp.org/2.0/en/core-utility-libraries/app.html#App::core - */ - public static function core($type) { - return array(CAKE . str_replace('/', DS, $type) . DS); - } - -/** - * Returns an array of objects of the given type. - * - * Example usage: - * - * `App::objects('plugin');` returns `array('DebugKit', 'Blog', 'User');` - * - * `App::objects('Controller');` returns `array('PagesController', 'BlogController');` - * - * You can also search only within a plugin's objects by using the plugin dot - * syntax. - * - * `App::objects('MyPlugin.Model');` returns `array('MyPluginPost', 'MyPluginComment');` - * - * When scanning directories, files and directories beginning with `.` will be excluded as these - * are commonly used by version control systems. - * - * @param string $type Type of object, i.e. 'Model', 'Controller', 'View/Helper', 'file', 'class' or 'plugin' - * @param string|array $path Optional Scan only the path given. If null, paths for the chosen type will be used. - * @param bool $cache Set to false to rescan objects of the chosen type. Defaults to true. - * @return mixed Either false on incorrect / miss. Or an array of found objects. - * @link https://book.cakephp.org/2.0/en/core-utility-libraries/app.html#App::objects - */ - public static function objects($type, $path = null, $cache = true) { - if (empty(static::$_objects) && $cache === true) { - static::$_objects = (array)Cache::read('object_map', '_cake_core_'); - } - - $extension = '/\.php$/'; - $includeDirectories = false; - $name = $type; - - if ($type === 'plugin') { - $type = 'plugins'; - } - - if ($type === 'plugins') { - $extension = '/.*/'; - $includeDirectories = true; - } - - list($plugin, $type) = pluginSplit($type); - - if (isset(static::$legacy[$type . 's'])) { - $type = static::$legacy[$type . 's']; - } - - if ($type === 'file' && !$path) { - return false; - } elseif ($type === 'file') { - $extension = '/\.php$/'; - $name = $type . str_replace(DS, '', $path); - } - - $cacheLocation = empty($plugin) ? 'app' : $plugin; - - if ($cache !== true || !isset(static::$_objects[$cacheLocation][$name])) { - $objects = array(); - - if (empty($path)) { - $path = static::path($type, $plugin); - } - - foreach ((array)$path as $dir) { - if ($dir != APP && is_dir($dir)) { - $files = new RegexIterator(new DirectoryIterator($dir), $extension); - foreach ($files as $file) { - $fileName = basename($file); - if (!$file->isDot() && $fileName[0] !== '.') { - $isDir = $file->isDir(); - if ($isDir && $includeDirectories) { - $objects[] = $fileName; - } elseif (!$includeDirectories && !$isDir) { - $objects[] = substr($fileName, 0, -4); - } - } - } - } - } - - if ($type !== 'file') { - foreach ($objects as $key => $value) { - $objects[$key] = Inflector::camelize($value); - } - } - - sort($objects); - if ($plugin) { - return $objects; - } - - static::$_objects[$cacheLocation][$name] = $objects; - if ($cache) { - static::$_objectCacheChange = true; - } - } - - return static::$_objects[$cacheLocation][$name]; - } - -/** - * Declares a package for a class. This package location will be used - * by the automatic class loader if the class is tried to be used - * - * Usage: - * - * `App::uses('MyCustomController', 'Controller');` will setup the class to be found under Controller package - * - * `App::uses('MyHelper', 'MyPlugin.View/Helper');` will setup the helper class to be found in plugin's helper package - * - * @param string $className the name of the class to configure package for - * @param string $location the package name - * @return void - * @link https://book.cakephp.org/2.0/en/core-utility-libraries/app.html#App::uses - */ - public static function uses($className, $location) { - static::$_classMap[$className] = $location; - } - -/** - * Method to handle the automatic class loading. It will look for each class' package - * defined using App::uses() and with this information it will resolve the package name to a full path - * to load the class from. File name for each class should follow the class name. For instance, - * if a class is name `MyCustomClass` the file name should be `MyCustomClass.php` - * - * @param string $className the name of the class to load - * @return bool - */ - public static function load($className) { - if (!isset(static::$_classMap[$className])) { - return false; - } - if (strpos($className, '..') !== false) { - return false; - } - - $parts = explode('.', static::$_classMap[$className], 2); - list($plugin, $package) = count($parts) > 1 ? $parts : array(null, current($parts)); - - $file = static::_mapped($className, $plugin); - if ($file) { - return include $file; - } - $paths = static::path($package, $plugin); - - if (empty($plugin)) { - $appLibs = empty(static::$_packages['Lib']) ? APPLIBS : current(static::$_packages['Lib']); - $paths[] = $appLibs . $package . DS; - $paths[] = APP . $package . DS; - $paths[] = CAKE . $package . DS; - } else { - $pluginPath = CakePlugin::path($plugin); - $paths[] = $pluginPath . 'Lib' . DS . $package . DS; - $paths[] = $pluginPath . $package . DS; - } - - $normalizedClassName = str_replace('\\', DS, $className); - foreach ($paths as $path) { - $file = $path . $normalizedClassName . '.php'; - if (file_exists($file)) { - static::_map($file, $className, $plugin); - return include $file; - } - } - - return false; - } - -/** - * Returns the package name where a class was defined to be located at - * - * @param string $className name of the class to obtain the package name from - * @return string|null Package name, or null if not declared - * @link https://book.cakephp.org/2.0/en/core-utility-libraries/app.html#App::location - */ - public static function location($className) { - if (!empty(static::$_classMap[$className])) { - return static::$_classMap[$className]; - } - return null; - } - -/** - * Finds classes based on $name or specific file(s) to search. Calling App::import() will - * not construct any classes contained in the files. It will only find and require() the file. - * - * @param string|array $type The type of Class if passed as a string, or all params can be passed as - * a single array to $type. - * @param string|array $name Name of the Class or a unique name for the file - * @param bool|array $parent boolean true if Class Parent should be searched, accepts key => value - * array('parent' => $parent, 'file' => $file, 'search' => $search, 'ext' => '$ext'); - * $ext allows setting the extension of the file name - * based on Inflector::underscore($name) . ".$ext"; - * @param array $search paths to search for files, array('path 1', 'path 2', 'path 3'); - * @param string $file full name of the file to search for including extension - * @param bool $return Return the loaded file, the file must have a return - * statement in it to work: return $variable; - * @return bool true if Class is already in memory or if file is found and loaded, false if not - * @link https://book.cakephp.org/2.0/en/core-utility-libraries/app.html#including-files-with-app-import - */ - public static function import($type = null, $name = null, $parent = true, $search = array(), $file = null, $return = false) { - $ext = null; - - if (is_array($type)) { - extract($type, EXTR_OVERWRITE); - } - - if (is_array($parent)) { - extract($parent, EXTR_OVERWRITE); - } - - if (!$name && !$file) { - return false; - } - - if (is_array($name)) { - foreach ($name as $class) { - if (!App::import(compact('type', 'parent', 'search', 'file', 'return') + array('name' => $class))) { - return false; - } - } - return true; - } - - $originalType = strtolower($type); - $specialPackage = in_array($originalType, array('file', 'vendor')); - if (!$specialPackage && isset(static::$legacy[$originalType . 's'])) { - $type = static::$legacy[$originalType . 's']; - } - list($plugin, $name) = pluginSplit($name); - if (!empty($plugin)) { - if (!CakePlugin::loaded($plugin)) { - return false; - } - } - - if (!$specialPackage) { - return static::_loadClass($name, $plugin, $type, $originalType, $parent); - } - - if ($originalType === 'file' && !empty($file)) { - return static::_loadFile($name, $plugin, $search, $file, $return); - } - - if ($originalType === 'vendor') { - return static::_loadVendor($name, $plugin, $file, $ext); - } - - return false; - } - -/** - * Helper function to include classes - * This is a compatibility wrapper around using App::uses() and automatic class loading - * - * @param string $name unique name of the file for identifying it inside the application - * @param string $plugin camel cased plugin name if any - * @param string $type name of the packed where the class is located - * @param string $originalType type name as supplied initially by the user - * @param bool $parent whether to load the class parent or not - * @return bool true indicating the successful load and existence of the class - */ - protected static function _loadClass($name, $plugin, $type, $originalType, $parent) { - if ($type === 'Console/Command' && $name === 'Shell') { - $type = 'Console'; - } elseif (isset(static::$types[$originalType]['suffix'])) { - $suffix = static::$types[$originalType]['suffix']; - $name .= ($suffix === $name) ? '' : $suffix; - } - if ($parent && isset(static::$types[$originalType]['extends'])) { - $extends = static::$types[$originalType]['extends']; - $extendType = $type; - if (strpos($extends, '/') !== false) { - $parts = explode('/', $extends); - $extends = array_pop($parts); - $extendType = implode('/', $parts); - } - App::uses($extends, $extendType); - if ($plugin && in_array($originalType, array('controller', 'model'))) { - App::uses($plugin . $extends, $plugin . '.' . $type); - } - } - if ($plugin) { - $plugin .= '.'; - } - $name = Inflector::camelize($name); - App::uses($name, $plugin . $type); - return class_exists($name); - } - -/** - * Helper function to include single files - * - * @param string $name unique name of the file for identifying it inside the application - * @param string $plugin camel cased plugin name if any - * @param array $search list of paths to search the file into - * @param string $file filename if known, the $name param will be used otherwise - * @param bool $return whether this function should return the contents of the file after being parsed by php or just a success notice - * @return mixed if $return contents of the file after php parses it, boolean indicating success otherwise - */ - protected static function _loadFile($name, $plugin, $search, $file, $return) { - $mapped = static::_mapped($name, $plugin); - if ($mapped) { - $file = $mapped; - } elseif (!empty($search)) { - foreach ($search as $path) { - $found = false; - if (file_exists($path . $file)) { - $file = $path . $file; - $found = true; - break; - } - if (empty($found)) { - $file = false; - } - } - } - if (!empty($file) && file_exists($file)) { - static::_map($file, $name, $plugin); - $returnValue = include $file; - if ($return) { - return $returnValue; - } - return (bool)$returnValue; - } - return false; - } - -/** - * Helper function to load files from vendors folders - * - * @param string $name unique name of the file for identifying it inside the application - * @param string $plugin camel cased plugin name if any - * @param string $file file name if known - * @param string $ext file extension if known - * @return bool true if the file was loaded successfully, false otherwise - */ - protected static function _loadVendor($name, $plugin, $file, $ext) { - if ($mapped = static::_mapped($name, $plugin)) { - return (bool)include_once $mapped; - } - $fileTries = array(); - $paths = ($plugin) ? App::path('vendors', $plugin) : App::path('vendors'); - if (empty($ext)) { - $ext = 'php'; - } - if (empty($file)) { - $fileTries[] = $name . '.' . $ext; - $fileTries[] = Inflector::underscore($name) . '.' . $ext; - } else { - $fileTries[] = $file; - } - - foreach ($fileTries as $file) { - foreach ($paths as $path) { - if (file_exists($path . $file)) { - static::_map($path . $file, $name, $plugin); - return (bool)include $path . $file; - } - } - } - return false; - } - -/** - * Initializes the cache for App, registers a shutdown function. - * - * @return void - */ - public static function init() { - static::$_map += (array)Cache::read('file_map', '_cake_core_'); - register_shutdown_function(array('App', 'shutdown')); - } - -/** - * Maps the $name to the $file. - * - * @param string $file full path to file - * @param string $name unique name for this map - * @param string $plugin camelized if object is from a plugin, the name of the plugin - * @return void - */ - protected static function _map($file, $name, $plugin = null) { - $key = $name; - if ($plugin) { - $key = 'plugin.' . $name; - } - if ($plugin && empty(static::$_map[$name])) { - static::$_map[$key] = $file; - } - if (!$plugin && empty(static::$_map['plugin.' . $name])) { - static::$_map[$key] = $file; - } - if (!static::$bootstrapping) { - static::$_cacheChange = true; - } - } - -/** - * Returns a file's complete path. - * - * @param string $name unique name - * @param string $plugin camelized if object is from a plugin, the name of the plugin - * @return mixed file path if found, false otherwise - */ - protected static function _mapped($name, $plugin = null) { - $key = $name; - if ($plugin) { - $key = 'plugin.' . $name; - } - return isset(static::$_map[$key]) ? static::$_map[$key] : false; - } - -/** - * Sets then returns the templates for each customizable package path - * - * @return array templates for each customizable package path - */ - protected static function _packageFormat() { - if (empty(static::$_packageFormat)) { - static::$_packageFormat = array( - 'Model' => array( - '%s' . 'Model' . DS - ), - 'Model/Behavior' => array( - '%s' . 'Model' . DS . 'Behavior' . DS - ), - 'Model/Datasource' => array( - '%s' . 'Model' . DS . 'Datasource' . DS - ), - 'Model/Datasource/Database' => array( - '%s' . 'Model' . DS . 'Datasource' . DS . 'Database' . DS - ), - 'Model/Datasource/Session' => array( - '%s' . 'Model' . DS . 'Datasource' . DS . 'Session' . DS - ), - 'Controller' => array( - '%s' . 'Controller' . DS - ), - 'Controller/Component' => array( - '%s' . 'Controller' . DS . 'Component' . DS - ), - 'Controller/Component/Auth' => array( - '%s' . 'Controller' . DS . 'Component' . DS . 'Auth' . DS - ), - 'Controller/Component/Acl' => array( - '%s' . 'Controller' . DS . 'Component' . DS . 'Acl' . DS - ), - 'View' => array( - '%s' . 'View' . DS - ), - 'View/Helper' => array( - '%s' . 'View' . DS . 'Helper' . DS - ), - 'Console' => array( - '%s' . 'Console' . DS - ), - 'Console/Command' => array( - '%s' . 'Console' . DS . 'Command' . DS - ), - 'Console/Command/Task' => array( - '%s' . 'Console' . DS . 'Command' . DS . 'Task' . DS - ), - 'Lib' => array( - '%s' . 'Lib' . DS - ), - 'Locale' => array( - '%s' . 'Locale' . DS - ), - 'Vendor' => array( - '%s' . 'Vendor' . DS, - ROOT . DS . 'vendors' . DS, - dirname(dirname(CAKE)) . DS . 'vendors' . DS - ), - 'Plugin' => array( - APP . 'Plugin' . DS, - ROOT . DS . 'plugins' . DS, - dirname(dirname(CAKE)) . DS . 'plugins' . DS - ) - ); - } - - return static::$_packageFormat; - } - -/** - * Increases the PHP "memory_limit" ini setting by the specified amount - * in kilobytes - * - * @param string $additionalKb Number in kilobytes - * @return void - */ - public static function increaseMemoryLimit($additionalKb) { - $limit = ini_get("memory_limit"); - if (!is_string($limit) || !strlen($limit)) { - return; - } - $limit = trim($limit); - $units = strtoupper(substr($limit, -1)); - $current = substr($limit, 0, strlen($limit) - 1); - if ($units === "M") { - $current = $current * 1024; - $units = "K"; - } - if ($units === "G") { - $current = $current * 1024 * 1024; - $units = "K"; - } - - if ($units === "K") { - ini_set("memory_limit", ceil($current + $additionalKb) . "K"); - } - } - -/** - * Object destructor. - * - * Writes cache file if changes have been made to the $_map. Also, check if a fatal - * error happened and call the handler. - * - * @return void - */ - public static function shutdown() { - $megabytes = Configure::read('Error.extraFatalErrorMemory'); - if ($megabytes === null) { - $megabytes = 4; - } - if ($megabytes !== false && $megabytes > 0) { - static::increaseMemoryLimit($megabytes * 1024); - } - - if (static::$_cacheChange) { - Cache::write('file_map', array_filter(static::$_map), '_cake_core_'); - } - if (static::$_objectCacheChange) { - Cache::write('object_map', static::$_objects, '_cake_core_'); - } - static::_checkFatalError(); - } - -/** - * Check if a fatal error happened and trigger the configured handler if configured - * - * @return void - */ - protected static function _checkFatalError() { - $lastError = error_get_last(); - if (!is_array($lastError)) { - return; - } - - list(, $log) = ErrorHandler::mapErrorCode($lastError['type']); - if ($log !== LOG_ERR) { - return; - } - - if (PHP_SAPI === 'cli') { - $errorHandler = Configure::read('Error.consoleHandler'); - } else { - $errorHandler = Configure::read('Error.handler'); - } - if (!is_callable($errorHandler)) { - return; - } - call_user_func($errorHandler, $lastError['type'], $lastError['message'], $lastError['file'], $lastError['line'], array()); - } +class App +{ + + /** + * Append paths + * + * @var string + */ + const APPEND = 'append'; + + /** + * Prepend paths + * + * @var string + */ + const PREPEND = 'prepend'; + + /** + * Register package + * + * @var string + */ + const REGISTER = 'register'; + + /** + * Reset paths instead of merging + * + * @var bool + */ + const RESET = true; + + /** + * List of object types and their properties + * + * @var array + */ + public static $types = [ + 'class' => ['extends' => null, 'core' => true], + 'file' => ['extends' => null, 'core' => true], + 'model' => ['extends' => 'AppModel', 'core' => false], + 'behavior' => ['suffix' => 'Behavior', 'extends' => 'Model/ModelBehavior', 'core' => true], + 'controller' => ['suffix' => 'Controller', 'extends' => 'AppController', 'core' => true], + 'component' => ['suffix' => 'Component', 'extends' => null, 'core' => true], + 'lib' => ['extends' => null, 'core' => true], + 'view' => ['suffix' => 'View', 'extends' => null, 'core' => true], + 'helper' => ['suffix' => 'Helper', 'extends' => 'AppHelper', 'core' => true], + 'vendor' => ['extends' => null, 'core' => true], + 'shell' => ['suffix' => 'Shell', 'extends' => 'AppShell', 'core' => true], + 'plugin' => ['extends' => null, 'core' => true] + ]; + + /** + * Paths to search for files. + * + * @var array + */ + public static $search = []; + + /** + * Whether or not to return the file that is loaded. + * + * @var bool + */ + public static $return = false; + /** + * Maps an old style CakePHP class type to the corresponding package + * + * @var array + */ + public static $legacy = [ + 'models' => 'Model', + 'behaviors' => 'Model/Behavior', + 'datasources' => 'Model/Datasource', + 'controllers' => 'Controller', + 'components' => 'Controller/Component', + 'views' => 'View', + 'helpers' => 'View/Helper', + 'shells' => 'Console/Command', + 'libs' => 'Lib', + 'vendors' => 'Vendor', + 'plugins' => 'Plugin', + 'locales' => 'Locale' + ]; + /** + * Indicates the the Application is in the bootstrapping process. Used to better cache + * loaded classes while the cache libraries have not been yet initialized + * + * @var bool + */ + public static $bootstrapping = false; + /** + * Holds key/value pairs of $type => file path. + * + * @var array + */ + protected static $_map = []; + /** + * Holds and key => value array of object types. + * + * @var array + */ + protected static $_objects = []; + /** + * Holds the location of each class + * + * @var array + */ + protected static $_classMap = []; + /** + * Holds the possible paths for each package name + * + * @var array + */ + protected static $_packages = []; + /** + * Holds the templates for each customizable package path in the application + * + * @var array + */ + protected static $_packageFormat = []; + /** + * Indicates whether the class cache should be stored again because of an addition to it + * + * @var bool + */ + protected static $_cacheChange = false; + /** + * Indicates whether the object cache should be stored again because of an addition to it + * + * @var bool + */ + protected static $_objectCacheChange = false; + + /** + * Get all the currently loaded paths from App. Useful for inspecting + * or storing all paths App knows about. For a paths to a specific package + * use App::path() + * + * @return array An array of packages and their associated paths. + * @link https://book.cakephp.org/2.0/en/core-utility-libraries/app.html#App::paths + */ + public static function paths() + { + return static::$_packages; + } + + /** + * Sets up each package location on the file system. You can configure multiple search paths + * for each package, those will be used to look for files one folder at a time in the specified order + * All paths should be terminated with a Directory separator + * + * Usage: + * + * `App::build(array('Model' => array('/a/full/path/to/models/'))); will setup a new search path for the Model package` + * + * `App::build(array('Model' => array('/path/to/models/')), App::RESET); will setup the path as the only valid path for searching models` + * + * `App::build(array('View/Helper' => array('/path/to/helpers/', '/another/path/'))); will setup multiple search paths for helpers` + * + * `App::build(array('Service' => array('%s' . 'Service' . DS)), App::REGISTER); will register new package 'Service'` + * + * If reset is set to true, all loaded plugins will be forgotten and they will be needed to be loaded again. + * + * @param array $paths associative array with package names as keys and a list of directories for new search paths + * @param bool|string $mode App::RESET will set paths, App::APPEND with append paths, App::PREPEND will prepend paths (default) + * App::REGISTER will register new packages and their paths, %s in path will be replaced by APP path + * @return void + * @link https://book.cakephp.org/2.0/en/core-utility-libraries/app.html#App::build + */ + public static function build($paths = [], $mode = App::PREPEND) + { + //Provides Backwards compatibility for old-style package names + $legacyPaths = []; + foreach ($paths as $type => $path) { + if (!empty(static::$legacy[$type])) { + $type = static::$legacy[$type]; + } + $legacyPaths[$type] = $path; + } + $paths = $legacyPaths; + + if ($mode === App::RESET) { + foreach ($paths as $type => $new) { + static::$_packages[$type] = (array)$new; + static::objects($type, null, false); + } + return; + } + + if (empty($paths)) { + static::$_packageFormat = null; + } + + $packageFormat = static::_packageFormat(); + + if ($mode === App::REGISTER) { + foreach ($paths as $package => $formats) { + if (empty($packageFormat[$package])) { + $packageFormat[$package] = $formats; + } else { + $formats = array_merge($packageFormat[$package], $formats); + $packageFormat[$package] = array_values(array_unique($formats)); + } + } + static::$_packageFormat = $packageFormat; + } + + $defaults = []; + foreach ($packageFormat as $package => $format) { + foreach ($format as $f) { + $defaults[$package][] = sprintf($f, APP); + } + } + + if (empty($paths)) { + static::$_packages = $defaults; + return; + } + + if ($mode === App::REGISTER) { + $paths = []; + } + + foreach ($defaults as $type => $default) { + if (!empty(static::$_packages[$type])) { + $path = static::$_packages[$type]; + } else { + $path = $default; + } + + if (!empty($paths[$type])) { + $newPath = (array)$paths[$type]; + + if ($mode === App::PREPEND) { + $path = array_merge($newPath, $path); + } else { + $path = array_merge($path, $newPath); + } + + $path = array_values(array_unique($path)); + } + + static::$_packages[$type] = $path; + } + } + + /** + * Returns an array of objects of the given type. + * + * Example usage: + * + * `App::objects('plugin');` returns `array('DebugKit', 'Blog', 'User');` + * + * `App::objects('Controller');` returns `array('PagesController', 'BlogController');` + * + * You can also search only within a plugin's objects by using the plugin dot + * syntax. + * + * `App::objects('MyPlugin.Model');` returns `array('MyPluginPost', 'MyPluginComment');` + * + * When scanning directories, files and directories beginning with `.` will be excluded as these + * are commonly used by version control systems. + * + * @param string $type Type of object, i.e. 'Model', 'Controller', 'View/Helper', 'file', 'class' or 'plugin' + * @param string|array $path Optional Scan only the path given. If null, paths for the chosen type will be used. + * @param bool $cache Set to false to rescan objects of the chosen type. Defaults to true. + * @return mixed Either false on incorrect / miss. Or an array of found objects. + * @link https://book.cakephp.org/2.0/en/core-utility-libraries/app.html#App::objects + */ + public static function objects($type, $path = null, $cache = true) + { + if (empty(static::$_objects) && $cache === true) { + static::$_objects = (array)Cache::read('object_map', '_cake_core_'); + } + + $extension = '/\.php$/'; + $includeDirectories = false; + $name = $type; + + if ($type === 'plugin') { + $type = 'plugins'; + } + + if ($type === 'plugins') { + $extension = '/.*/'; + $includeDirectories = true; + } + + list($plugin, $type) = pluginSplit($type); + + if (isset(static::$legacy[$type . 's'])) { + $type = static::$legacy[$type . 's']; + } + + if ($type === 'file' && !$path) { + return false; + } else if ($type === 'file') { + $extension = '/\.php$/'; + $name = $type . str_replace(DS, '', $path); + } + + $cacheLocation = empty($plugin) ? 'app' : $plugin; + + if ($cache !== true || !isset(static::$_objects[$cacheLocation][$name])) { + $objects = []; + + if (empty($path)) { + $path = static::path($type, $plugin); + } + + foreach ((array)$path as $dir) { + if ($dir != APP && is_dir($dir)) { + $files = new RegexIterator(new DirectoryIterator($dir), $extension); + foreach ($files as $file) { + $fileName = basename($file); + if (!$file->isDot() && $fileName[0] !== '.') { + $isDir = $file->isDir(); + if ($isDir && $includeDirectories) { + $objects[] = $fileName; + } else if (!$includeDirectories && !$isDir) { + $objects[] = substr($fileName, 0, -4); + } + } + } + } + } + + if ($type !== 'file') { + foreach ($objects as $key => $value) { + $objects[$key] = Inflector::camelize($value); + } + } + + sort($objects); + if ($plugin) { + return $objects; + } + + static::$_objects[$cacheLocation][$name] = $objects; + if ($cache) { + static::$_objectCacheChange = true; + } + } + + return static::$_objects[$cacheLocation][$name]; + } + + /** + * Used to read information stored path + * + * Usage: + * + * `App::path('Model'); will return all paths for models` + * + * `App::path('Model/Datasource', 'MyPlugin'); will return the path for datasources under the 'MyPlugin' plugin` + * + * @param string $type type of path + * @param string $plugin name of plugin + * @return array + * @link https://book.cakephp.org/2.0/en/core-utility-libraries/app.html#App::path + */ + public static function path($type, $plugin = null) + { + if (!empty(static::$legacy[$type])) { + $type = static::$legacy[$type]; + } + + if (!empty($plugin)) { + $path = []; + $pluginPath = CakePlugin::path($plugin); + $packageFormat = static::_packageFormat(); + if (!empty($packageFormat[$type])) { + foreach ($packageFormat[$type] as $f) { + $path[] = sprintf($f, $pluginPath); + } + } + return $path; + } + + if (!isset(static::$_packages[$type])) { + return []; + } + return static::$_packages[$type]; + } + + /** + * Sets then returns the templates for each customizable package path + * + * @return array templates for each customizable package path + */ + protected static function _packageFormat() + { + if (empty(static::$_packageFormat)) { + static::$_packageFormat = [ + 'Model' => [ + '%s' . 'Model' . DS + ], + 'Model/Behavior' => [ + '%s' . 'Model' . DS . 'Behavior' . DS + ], + 'Model/Datasource' => [ + '%s' . 'Model' . DS . 'Datasource' . DS + ], + 'Model/Datasource/Database' => [ + '%s' . 'Model' . DS . 'Datasource' . DS . 'Database' . DS + ], + 'Model/Datasource/Session' => [ + '%s' . 'Model' . DS . 'Datasource' . DS . 'Session' . DS + ], + 'Controller' => [ + '%s' . 'Controller' . DS + ], + 'Controller/Component' => [ + '%s' . 'Controller' . DS . 'Component' . DS + ], + 'Controller/Component/Auth' => [ + '%s' . 'Controller' . DS . 'Component' . DS . 'Auth' . DS + ], + 'Controller/Component/Acl' => [ + '%s' . 'Controller' . DS . 'Component' . DS . 'Acl' . DS + ], + 'View' => [ + '%s' . 'View' . DS + ], + 'View/Helper' => [ + '%s' . 'View' . DS . 'Helper' . DS + ], + 'Console' => [ + '%s' . 'Console' . DS + ], + 'Console/Command' => [ + '%s' . 'Console' . DS . 'Command' . DS + ], + 'Console/Command/Task' => [ + '%s' . 'Console' . DS . 'Command' . DS . 'Task' . DS + ], + 'Lib' => [ + '%s' . 'Lib' . DS + ], + 'Locale' => [ + '%s' . 'Locale' . DS + ], + 'Vendor' => [ + '%s' . 'Vendor' . DS, + ROOT . DS . 'vendors' . DS, + dirname(dirname(CAKE)) . DS . 'vendors' . DS + ], + 'Plugin' => [ + APP . 'Plugin' . DS, + ROOT . DS . 'plugins' . DS, + dirname(dirname(CAKE)) . DS . 'plugins' . DS + ] + ]; + } + + return static::$_packageFormat; + } + + /** + * Gets the path that a plugin is on. Searches through the defined plugin paths. + * + * Usage: + * + * `App::pluginPath('MyPlugin'); will return the full path to 'MyPlugin' plugin'` + * + * @param string $plugin CamelCased/lower_cased plugin name to find the path of. + * @return string full path to the plugin. + * @link https://book.cakephp.org/2.0/en/core-utility-libraries/app.html#App::pluginPath + * @deprecated 3.0.0 Use `CakePlugin::path()` instead. + */ + public static function pluginPath($plugin) + { + return CakePlugin::path($plugin); + } + + /** + * Finds the path that a theme is on. Searches through the defined theme paths. + * + * Usage: + * + * `App::themePath('MyTheme'); will return the full path to the 'MyTheme' theme` + * + * @param string $theme theme name to find the path of. + * @return string full path to the theme. + * @link https://book.cakephp.org/2.0/en/core-utility-libraries/app.html#App::themePath + */ + public static function themePath($theme) + { + $themeDir = 'Themed' . DS . Inflector::camelize($theme); + foreach (static::$_packages['View'] as $path) { + if (is_dir($path . $themeDir)) { + return $path . $themeDir . DS; + } + } + return static::$_packages['View'][0] . $themeDir . DS; + } + + /** + * Returns the full path to a package inside the CakePHP core + * + * Usage: + * + * `App::core('Cache/Engine'); will return the full path to the cache engines package` + * + * @param string $type Package type. + * @return array full path to package + * @link https://book.cakephp.org/2.0/en/core-utility-libraries/app.html#App::core + */ + public static function core($type) + { + return [CAKE . str_replace('/', DS, $type) . DS]; + } + + /** + * Method to handle the automatic class loading. It will look for each class' package + * defined using App::uses() and with this information it will resolve the package name to a full path + * to load the class from. File name for each class should follow the class name. For instance, + * if a class is name `MyCustomClass` the file name should be `MyCustomClass.php` + * + * @param string $className the name of the class to load + * @return bool + */ + public static function load($className) + { + if (!isset(static::$_classMap[$className])) { + return false; + } + if (strpos($className, '..') !== false) { + return false; + } + + $parts = explode('.', static::$_classMap[$className], 2); + list($plugin, $package) = count($parts) > 1 ? $parts : [null, current($parts)]; + + $file = static::_mapped($className, $plugin); + if ($file) { + return include $file; + } + $paths = static::path($package, $plugin); + + if (empty($plugin)) { + $appLibs = empty(static::$_packages['Lib']) ? APPLIBS : current(static::$_packages['Lib']); + $paths[] = $appLibs . $package . DS; + $paths[] = APP . $package . DS; + $paths[] = CAKE . $package . DS; + } else { + $pluginPath = CakePlugin::path($plugin); + $paths[] = $pluginPath . 'Lib' . DS . $package . DS; + $paths[] = $pluginPath . $package . DS; + } + + $normalizedClassName = str_replace('\\', DS, $className); + foreach ($paths as $path) { + $file = $path . $normalizedClassName . '.php'; + if (file_exists($file)) { + static::_map($file, $className, $plugin); + return include $file; + } + } + + return false; + } + + /** + * Returns a file's complete path. + * + * @param string $name unique name + * @param string $plugin camelized if object is from a plugin, the name of the plugin + * @return mixed file path if found, false otherwise + */ + protected static function _mapped($name, $plugin = null) + { + $key = $name; + if ($plugin) { + $key = 'plugin.' . $name; + } + return isset(static::$_map[$key]) ? static::$_map[$key] : false; + } + + /** + * Maps the $name to the $file. + * + * @param string $file full path to file + * @param string $name unique name for this map + * @param string $plugin camelized if object is from a plugin, the name of the plugin + * @return void + */ + protected static function _map($file, $name, $plugin = null) + { + $key = $name; + if ($plugin) { + $key = 'plugin.' . $name; + } + if ($plugin && empty(static::$_map[$name])) { + static::$_map[$key] = $file; + } + if (!$plugin && empty(static::$_map['plugin.' . $name])) { + static::$_map[$key] = $file; + } + if (!static::$bootstrapping) { + static::$_cacheChange = true; + } + } + + /** + * Returns the package name where a class was defined to be located at + * + * @param string $className name of the class to obtain the package name from + * @return string|null Package name, or null if not declared + * @link https://book.cakephp.org/2.0/en/core-utility-libraries/app.html#App::location + */ + public static function location($className) + { + if (!empty(static::$_classMap[$className])) { + return static::$_classMap[$className]; + } + return null; + } + + /** + * Finds classes based on $name or specific file(s) to search. Calling App::import() will + * not construct any classes contained in the files. It will only find and require() the file. + * + * @param string|array $type The type of Class if passed as a string, or all params can be passed as + * a single array to $type. + * @param string|array $name Name of the Class or a unique name for the file + * @param bool|array $parent boolean true if Class Parent should be searched, accepts key => value + * array('parent' => $parent, 'file' => $file, 'search' => $search, 'ext' => '$ext'); + * $ext allows setting the extension of the file name + * based on Inflector::underscore($name) . ".$ext"; + * @param array $search paths to search for files, array('path 1', 'path 2', 'path 3'); + * @param string $file full name of the file to search for including extension + * @param bool $return Return the loaded file, the file must have a return + * statement in it to work: return $variable; + * @return bool true if Class is already in memory or if file is found and loaded, false if not + * @link https://book.cakephp.org/2.0/en/core-utility-libraries/app.html#including-files-with-app-import + */ + public static function import($type = null, $name = null, $parent = true, $search = [], $file = null, $return = false) + { + $ext = null; + + if (is_array($type)) { + extract($type, EXTR_OVERWRITE); + } + + if (is_array($parent)) { + extract($parent, EXTR_OVERWRITE); + } + + if (!$name && !$file) { + return false; + } + + if (is_array($name)) { + foreach ($name as $class) { + if (!App::import(compact('type', 'parent', 'search', 'file', 'return') + ['name' => $class])) { + return false; + } + } + return true; + } + + $originalType = strtolower($type); + $specialPackage = in_array($originalType, ['file', 'vendor']); + if (!$specialPackage && isset(static::$legacy[$originalType . 's'])) { + $type = static::$legacy[$originalType . 's']; + } + list($plugin, $name) = pluginSplit($name); + if (!empty($plugin)) { + if (!CakePlugin::loaded($plugin)) { + return false; + } + } + + if (!$specialPackage) { + return static::_loadClass($name, $plugin, $type, $originalType, $parent); + } + + if ($originalType === 'file' && !empty($file)) { + return static::_loadFile($name, $plugin, $search, $file, $return); + } + + if ($originalType === 'vendor') { + return static::_loadVendor($name, $plugin, $file, $ext); + } + + return false; + } + + /** + * Helper function to include classes + * This is a compatibility wrapper around using App::uses() and automatic class loading + * + * @param string $name unique name of the file for identifying it inside the application + * @param string $plugin camel cased plugin name if any + * @param string $type name of the packed where the class is located + * @param string $originalType type name as supplied initially by the user + * @param bool $parent whether to load the class parent or not + * @return bool true indicating the successful load and existence of the class + */ + protected static function _loadClass($name, $plugin, $type, $originalType, $parent) + { + if ($type === 'Console/Command' && $name === 'Shell') { + $type = 'Console'; + } else if (isset(static::$types[$originalType]['suffix'])) { + $suffix = static::$types[$originalType]['suffix']; + $name .= ($suffix === $name) ? '' : $suffix; + } + if ($parent && isset(static::$types[$originalType]['extends'])) { + $extends = static::$types[$originalType]['extends']; + $extendType = $type; + if (strpos($extends, '/') !== false) { + $parts = explode('/', $extends); + $extends = array_pop($parts); + $extendType = implode('/', $parts); + } + App::uses($extends, $extendType); + if ($plugin && in_array($originalType, ['controller', 'model'])) { + App::uses($plugin . $extends, $plugin . '.' . $type); + } + } + if ($plugin) { + $plugin .= '.'; + } + $name = Inflector::camelize($name); + App::uses($name, $plugin . $type); + return class_exists($name); + } + + /** + * Declares a package for a class. This package location will be used + * by the automatic class loader if the class is tried to be used + * + * Usage: + * + * `App::uses('MyCustomController', 'Controller');` will setup the class to be found under Controller package + * + * `App::uses('MyHelper', 'MyPlugin.View/Helper');` will setup the helper class to be found in plugin's helper package + * + * @param string $className the name of the class to configure package for + * @param string $location the package name + * @return void + * @link https://book.cakephp.org/2.0/en/core-utility-libraries/app.html#App::uses + */ + public static function uses($className, $location) + { + static::$_classMap[$className] = $location; + } + + /** + * Helper function to include single files + * + * @param string $name unique name of the file for identifying it inside the application + * @param string $plugin camel cased plugin name if any + * @param array $search list of paths to search the file into + * @param string $file filename if known, the $name param will be used otherwise + * @param bool $return whether this function should return the contents of the file after being parsed by php or just a success notice + * @return mixed if $return contents of the file after php parses it, boolean indicating success otherwise + */ + protected static function _loadFile($name, $plugin, $search, $file, $return) + { + $mapped = static::_mapped($name, $plugin); + if ($mapped) { + $file = $mapped; + } else if (!empty($search)) { + foreach ($search as $path) { + $found = false; + if (file_exists($path . $file)) { + $file = $path . $file; + $found = true; + break; + } + if (empty($found)) { + $file = false; + } + } + } + if (!empty($file) && file_exists($file)) { + static::_map($file, $name, $plugin); + $returnValue = include $file; + if ($return) { + return $returnValue; + } + return (bool)$returnValue; + } + return false; + } + + /** + * Helper function to load files from vendors folders + * + * @param string $name unique name of the file for identifying it inside the application + * @param string $plugin camel cased plugin name if any + * @param string $file file name if known + * @param string $ext file extension if known + * @return bool true if the file was loaded successfully, false otherwise + */ + protected static function _loadVendor($name, $plugin, $file, $ext) + { + if ($mapped = static::_mapped($name, $plugin)) { + return (bool)include_once $mapped; + } + $fileTries = []; + $paths = ($plugin) ? App::path('vendors', $plugin) : App::path('vendors'); + if (empty($ext)) { + $ext = 'php'; + } + if (empty($file)) { + $fileTries[] = $name . '.' . $ext; + $fileTries[] = Inflector::underscore($name) . '.' . $ext; + } else { + $fileTries[] = $file; + } + + foreach ($fileTries as $file) { + foreach ($paths as $path) { + if (file_exists($path . $file)) { + static::_map($path . $file, $name, $plugin); + return (bool)include $path . $file; + } + } + } + return false; + } + + /** + * Initializes the cache for App, registers a shutdown function. + * + * @return void + */ + public static function init() + { + static::$_map += (array)Cache::read('file_map', '_cake_core_'); + register_shutdown_function(['App', 'shutdown']); + } + + /** + * Object destructor. + * + * Writes cache file if changes have been made to the $_map. Also, check if a fatal + * error happened and call the handler. + * + * @return void + */ + public static function shutdown() + { + $megabytes = Configure::read('Error.extraFatalErrorMemory'); + if ($megabytes === null) { + $megabytes = 4; + } + if ($megabytes !== false && $megabytes > 0) { + static::increaseMemoryLimit($megabytes * 1024); + } + + if (static::$_cacheChange) { + Cache::write('file_map', array_filter(static::$_map), '_cake_core_'); + } + if (static::$_objectCacheChange) { + Cache::write('object_map', static::$_objects, '_cake_core_'); + } + static::_checkFatalError(); + } + + /** + * Increases the PHP "memory_limit" ini setting by the specified amount + * in kilobytes + * + * @param string $additionalKb Number in kilobytes + * @return void + */ + public static function increaseMemoryLimit($additionalKb) + { + $limit = ini_get("memory_limit"); + if (!is_string($limit) || !strlen($limit)) { + return; + } + $limit = trim($limit); + $units = strtoupper(substr($limit, -1)); + $current = substr($limit, 0, strlen($limit) - 1); + if ($units === "M") { + $current = $current * 1024; + $units = "K"; + } + if ($units === "G") { + $current = $current * 1024 * 1024; + $units = "K"; + } + + if ($units === "K") { + ini_set("memory_limit", ceil($current + $additionalKb) . "K"); + } + } + + /** + * Check if a fatal error happened and trigger the configured handler if configured + * + * @return void + */ + protected static function _checkFatalError() + { + $lastError = error_get_last(); + if (!is_array($lastError)) { + return; + } + + list(, $log) = ErrorHandler::mapErrorCode($lastError['type']); + if ($log !== LOG_ERR) { + return; + } + + if (PHP_SAPI === 'cli') { + $errorHandler = Configure::read('Error.consoleHandler'); + } else { + $errorHandler = Configure::read('Error.handler'); + } + if (!is_callable($errorHandler)) { + return; + } + call_user_func($errorHandler, $lastError['type'], $lastError['message'], $lastError['file'], $lastError['line'], []); + } } diff --git a/lib/Cake/Core/CakeObject.php b/lib/Cake/Core/CakeObject.php index 0a0f3ac1..3018a3d7 100755 --- a/lib/Cake/Core/CakeObject.php +++ b/lib/Cake/Core/CakeObject.php @@ -27,186 +27,195 @@ * * @package Cake.Core */ -class CakeObject { +class CakeObject +{ -/** - * Constructor, no-op - */ - public function __construct() { - } + /** + * Constructor, no-op + */ + public function __construct() + { + } -/** - * CakeObject-to-string conversion. - * Each class can override this method as necessary. - * - * @return string The name of this class - */ - public function toString() { - $class = get_class($this); - return $class; - } + /** + * CakeObject-to-string conversion. + * Each class can override this method as necessary. + * + * @return string The name of this class + */ + public function toString() + { + $class = get_class($this); + return $class; + } -/** - * Calls a controller's method from any location. Can be used to connect controllers together - * or tie plugins into a main application. requestAction can be used to return rendered views - * or fetch the return value from controller actions. - * - * Under the hood this method uses Router::reverse() to convert the $url parameter into a string - * URL. You should use URL formats that are compatible with Router::reverse() - * - * #### Passing POST and GET data - * - * POST and GET data can be simulated in requestAction. Use `$extra['url']` for - * GET data. The `$extra['data']` parameter allows POST data simulation. - * - * @param string|array $url String or array-based URL. Unlike other URL arrays in CakePHP, this - * URL will not automatically handle passed and named arguments in the $url parameter. - * @param array $extra if array includes the key "return" it sets the AutoRender to true. Can - * also be used to submit GET/POST data, and named/passed arguments. - * @return mixed Boolean true or false on success/failure, or contents - * of rendered action if 'return' is set in $extra. - */ - public function requestAction($url, $extra = array()) { - if (empty($url)) { - return false; - } - if (($index = array_search('return', $extra)) !== false) { - $extra['return'] = 0; - $extra['autoRender'] = 1; - unset($extra[$index]); - } - $arrayUrl = is_array($url); - if ($arrayUrl && !isset($extra['url'])) { - $extra['url'] = array(); - } - if ($arrayUrl && !isset($extra['data'])) { - $extra['data'] = array(); - } - $extra += array('autoRender' => 0, 'return' => 1, 'bare' => 1, 'requested' => 1); - $data = isset($extra['data']) ? $extra['data'] : null; - unset($extra['data']); + /** + * Calls a controller's method from any location. Can be used to connect controllers together + * or tie plugins into a main application. requestAction can be used to return rendered views + * or fetch the return value from controller actions. + * + * Under the hood this method uses Router::reverse() to convert the $url parameter into a string + * URL. You should use URL formats that are compatible with Router::reverse() + * + * #### Passing POST and GET data + * + * POST and GET data can be simulated in requestAction. Use `$extra['url']` for + * GET data. The `$extra['data']` parameter allows POST data simulation. + * + * @param string|array $url String or array-based URL. Unlike other URL arrays in CakePHP, this + * URL will not automatically handle passed and named arguments in the $url parameter. + * @param array $extra if array includes the key "return" it sets the AutoRender to true. Can + * also be used to submit GET/POST data, and named/passed arguments. + * @return mixed Boolean true or false on success/failure, or contents + * of rendered action if 'return' is set in $extra. + */ + public function requestAction($url, $extra = []) + { + if (empty($url)) { + return false; + } + if (($index = array_search('return', $extra)) !== false) { + $extra['return'] = 0; + $extra['autoRender'] = 1; + unset($extra[$index]); + } + $arrayUrl = is_array($url); + if ($arrayUrl && !isset($extra['url'])) { + $extra['url'] = []; + } + if ($arrayUrl && !isset($extra['data'])) { + $extra['data'] = []; + } + $extra += ['autoRender' => 0, 'return' => 1, 'bare' => 1, 'requested' => 1]; + $data = isset($extra['data']) ? $extra['data'] : null; + unset($extra['data']); - if (is_string($url) && strpos($url, Router::fullBaseUrl()) === 0) { - $url = Router::normalize(str_replace(Router::fullBaseUrl(), '', $url)); - } - if (is_string($url)) { - $request = new CakeRequest($url); - } elseif (is_array($url)) { - $params = $url + array('pass' => array(), 'named' => array(), 'base' => false); - $params = $extra + $params; - $request = new CakeRequest(Router::reverse($params)); - } - if (isset($data)) { - $request->data = $data; - } + if (is_string($url) && strpos($url, Router::fullBaseUrl()) === 0) { + $url = Router::normalize(str_replace(Router::fullBaseUrl(), '', $url)); + } + if (is_string($url)) { + $request = new CakeRequest($url); + } else if (is_array($url)) { + $params = $url + ['pass' => [], 'named' => [], 'base' => false]; + $params = $extra + $params; + $request = new CakeRequest(Router::reverse($params)); + } + if (isset($data)) { + $request->data = $data; + } - $dispatcher = new Dispatcher(); - $result = $dispatcher->dispatch($request, new CakeResponse(), $extra); - Router::popRequest(); - return $result; - } + $dispatcher = new Dispatcher(); + $result = $dispatcher->dispatch($request, new CakeResponse(), $extra); + Router::popRequest(); + return $result; + } -/** - * Calls a method on this object with the given parameters. Provides an OO wrapper - * for `call_user_func_array` - * - * @param string $method Name of the method to call - * @param array $params Parameter list to use when calling $method - * @return mixed Returns the result of the method call - */ - public function dispatchMethod($method, $params = array()) { - switch (count($params)) { - case 0: - return $this->{$method}(); - case 1: - return $this->{$method}($params[0]); - case 2: - return $this->{$method}($params[0], $params[1]); - case 3: - return $this->{$method}($params[0], $params[1], $params[2]); - case 4: - return $this->{$method}($params[0], $params[1], $params[2], $params[3]); - case 5: - return $this->{$method}($params[0], $params[1], $params[2], $params[3], $params[4]); - default: - return call_user_func_array(array(&$this, $method), $params); - } - } + /** + * Calls a method on this object with the given parameters. Provides an OO wrapper + * for `call_user_func_array` + * + * @param string $method Name of the method to call + * @param array $params Parameter list to use when calling $method + * @return mixed Returns the result of the method call + */ + public function dispatchMethod($method, $params = []) + { + switch (count($params)) { + case 0: + return $this->{$method}(); + case 1: + return $this->{$method}($params[0]); + case 2: + return $this->{$method}($params[0], $params[1]); + case 3: + return $this->{$method}($params[0], $params[1], $params[2]); + case 4: + return $this->{$method}($params[0], $params[1], $params[2], $params[3]); + case 5: + return $this->{$method}($params[0], $params[1], $params[2], $params[3], $params[4]); + default: + return call_user_func_array([&$this, $method], $params); + } + } -/** - * Stop execution of the current script. Wraps exit() making - * testing easier. - * - * @param int|string $status see http://php.net/exit for values - * @return void - */ - protected function _stop($status = 0) { - exit($status); - } + /** + * Convenience method to write a message to CakeLog. See CakeLog::write() + * for more information on writing to logs. + * + * @param mixed $msg Log message + * @param int $type Error type constant. Defined in app/Config/core.php. + * @param null|string|array $scope The scope(s) a log message is being created in. + * See CakeLog::config() for more information on logging scopes. + * @return bool Success of log write + */ + public function log($msg, $type = LOG_ERR, $scope = null) + { + if (!is_string($msg)) { + $msg = print_r($msg, true); + } -/** - * Convenience method to write a message to CakeLog. See CakeLog::write() - * for more information on writing to logs. - * - * @param mixed $msg Log message - * @param int $type Error type constant. Defined in app/Config/core.php. - * @param null|string|array $scope The scope(s) a log message is being created in. - * See CakeLog::config() for more information on logging scopes. - * @return bool Success of log write - */ - public function log($msg, $type = LOG_ERR, $scope = null) { - if (!is_string($msg)) { - $msg = print_r($msg, true); - } + return CakeLog::write($type, $msg, $scope); + } - return CakeLog::write($type, $msg, $scope); - } + /** + * Stop execution of the current script. Wraps exit() making + * testing easier. + * + * @param int|string $status see http://php.net/exit for values + * @return void + */ + protected function _stop($status = 0) + { + exit($status); + } -/** - * Allows setting of multiple properties of the object in a single line of code. Will only set - * properties that are part of a class declaration. - * - * @param array $properties An associative array containing properties and corresponding values. - * @return void - */ - protected function _set($properties = array()) { - if (is_array($properties) && !empty($properties)) { - $vars = get_object_vars($this); - foreach ($properties as $key => $val) { - if (array_key_exists($key, $vars)) { - $this->{$key} = $val; - } - } - } - } + /** + * Allows setting of multiple properties of the object in a single line of code. Will only set + * properties that are part of a class declaration. + * + * @param array $properties An associative array containing properties and corresponding values. + * @return void + */ + protected function _set($properties = []) + { + if (is_array($properties) && !empty($properties)) { + $vars = get_object_vars($this); + foreach ($properties as $key => $val) { + if (array_key_exists($key, $vars)) { + $this->{$key} = $val; + } + } + } + } -/** - * Merges this objects $property with the property in $class' definition. - * This classes value for the property will be merged on top of $class' - * - * This provides some of the DRY magic CakePHP provides. If you want to shut it off, redefine - * this method as an empty function. - * - * @param array $properties The name of the properties to merge. - * @param string $class The class to merge the property with. - * @param bool $normalize Set to true to run the properties through Hash::normalize() before merging. - * @return void - */ - protected function _mergeVars($properties, $class, $normalize = true) { - $classProperties = get_class_vars($class); - foreach ($properties as $var) { - if (isset($classProperties[$var]) && - !empty($classProperties[$var]) && - is_array($this->{$var}) && - $this->{$var} != $classProperties[$var] - ) { - if ($normalize) { - $classProperties[$var] = Hash::normalize($classProperties[$var]); - $this->{$var} = Hash::normalize($this->{$var}); - } - $this->{$var} = Hash::merge($classProperties[$var], $this->{$var}); - } - } - } + /** + * Merges this objects $property with the property in $class' definition. + * This classes value for the property will be merged on top of $class' + * + * This provides some of the DRY magic CakePHP provides. If you want to shut it off, redefine + * this method as an empty function. + * + * @param array $properties The name of the properties to merge. + * @param string $class The class to merge the property with. + * @param bool $normalize Set to true to run the properties through Hash::normalize() before merging. + * @return void + */ + protected function _mergeVars($properties, $class, $normalize = true) + { + $classProperties = get_class_vars($class); + foreach ($properties as $var) { + if (isset($classProperties[$var]) && + !empty($classProperties[$var]) && + is_array($this->{$var}) && + $this->{$var} != $classProperties[$var] + ) { + if ($normalize) { + $classProperties[$var] = Hash::normalize($classProperties[$var]); + $this->{$var} = Hash::normalize($this->{$var}); + } + $this->{$var} = Hash::merge($classProperties[$var], $this->{$var}); + } + } + } } diff --git a/lib/Cake/Core/CakePlugin.php b/lib/Cake/Core/CakePlugin.php index ff2897ef..7f86c892 100755 --- a/lib/Cake/Core/CakePlugin.php +++ b/lib/Cake/Core/CakePlugin.php @@ -24,266 +24,275 @@ * @package Cake.Core * @link https://book.cakephp.org/2.0/en/plugins.html */ -class CakePlugin { +class CakePlugin +{ -/** - * Holds a list of all loaded plugins and their configuration - * - * @var array - */ - protected static $_plugins = array(); + /** + * Holds a list of all loaded plugins and their configuration + * + * @var array + */ + protected static $_plugins = []; -/** - * Loads a plugin and optionally loads bootstrapping, routing files or loads an initialization function - * - * Examples: - * - * `CakePlugin::load('DebugKit');` - * - * Will load the DebugKit plugin and will not load any bootstrap nor route files. - * - * `CakePlugin::load('DebugKit', array('bootstrap' => true, 'routes' => true));` - * - * Will load the bootstrap.php and routes.php files. - * - * `CakePlugin::load('DebugKit', array('bootstrap' => false, 'routes' => true));` - * - * Will load routes.php file but not bootstrap.php. - * - * `CakePlugin::load('DebugKit', array('bootstrap' => array('config1', 'config2')));` - * - * Will load config1.php and config2.php files. - * - * `CakePlugin::load('DebugKit', array('bootstrap' => 'aCallableMethod'));` - * - * Will run the aCallableMethod function to initialize it. - * - * Bootstrap initialization functions can be expressed as a PHP callback type, - * including closures. Callbacks will receive two parameters - * (plugin name, plugin configuration). - * - * It is also possible to load multiple plugins at once. Examples: - * - * `CakePlugin::load(array('DebugKit', 'ApiGenerator'));` - * - * Will load the DebugKit and ApiGenerator plugins. - * - * `CakePlugin::load(array('DebugKit', 'ApiGenerator'), array('bootstrap' => true));` - * - * Will load bootstrap file for both plugins. - * - * ``` - * CakePlugin::load(array( - * 'DebugKit' => array('routes' => true), - * 'ApiGenerator' - * ), - * array('bootstrap' => true) - * ); - * ``` - * - * Will only load the bootstrap for ApiGenerator and only the routes for DebugKit. - * By using the `path` option you can specify an absolute path to the plugin. Make - * sure that the path is slash terminated or your plugin will not be located properly. - * - * @param string|array $plugin name of the plugin to be loaded in CamelCase format or array or plugins to load - * @param array $config configuration options for the plugin - * @throws MissingPluginException if the folder for the plugin to be loaded is not found - * @return void - */ - public static function load($plugin, $config = array()) { - if (is_array($plugin)) { - foreach ($plugin as $name => $conf) { - list($name, $conf) = (is_numeric($name)) ? array($conf, $config) : array($name, $conf); - static::load($name, $conf); - } - return; - } - $config += array('bootstrap' => false, 'routes' => false, 'ignoreMissing' => false); - if (empty($config['path'])) { - foreach (App::path('plugins') as $path) { - if (is_dir($path . $plugin)) { - static::$_plugins[$plugin] = $config + array('path' => $path . $plugin . DS); - break; - } + /** + * Will load all the plugins located in the configured plugins folders + * + * If passed an options array, it will be used as a common default for all plugins to be loaded + * It is possible to set specific defaults for each plugins in the options array. Examples: + * + * ``` + * CakePlugin::loadAll(array( + * array('bootstrap' => true), + * 'DebugKit' => array('routes' => true, 'bootstrap' => false), + * )); + * ``` + * + * The above example will load the bootstrap file for all plugins, but for DebugKit it will only load + * the routes file and will not look for any bootstrap script. If you are loading + * many plugins that inconsistently support routes/bootstrap files, instead of detailing + * each plugin you can use the `ignoreMissing` option: + * + * ``` + * CakePlugin::loadAll(array( + * 'ignoreMissing' => true, + * 'bootstrap' => true, + * 'routes' => true, + * )); + * ``` + * + * The ignoreMissing option will do additional file_exists() calls but is simpler + * to use. + * + * @param array $options Options list. See CakePlugin::load() for valid options. + * @return void + */ + public static function loadAll($options = []) + { + $plugins = App::objects('plugins'); + foreach ($plugins as $plugin) { + $pluginOptions = isset($options[$plugin]) ? (array)$options[$plugin] : []; + if (isset($options[0])) { + $pluginOptions += $options[0]; + } + static::load($plugin, $pluginOptions); + } + } - //Backwards compatibility to make easier to migrate to 2.0 - $underscored = Inflector::underscore($plugin); - if (is_dir($path . $underscored)) { - static::$_plugins[$plugin] = $config + array('path' => $path . $underscored . DS); - break; - } - } - } else { - static::$_plugins[$plugin] = $config; - } + /** + * Loads a plugin and optionally loads bootstrapping, routing files or loads an initialization function + * + * Examples: + * + * `CakePlugin::load('DebugKit');` + * + * Will load the DebugKit plugin and will not load any bootstrap nor route files. + * + * `CakePlugin::load('DebugKit', array('bootstrap' => true, 'routes' => true));` + * + * Will load the bootstrap.php and routes.php files. + * + * `CakePlugin::load('DebugKit', array('bootstrap' => false, 'routes' => true));` + * + * Will load routes.php file but not bootstrap.php. + * + * `CakePlugin::load('DebugKit', array('bootstrap' => array('config1', 'config2')));` + * + * Will load config1.php and config2.php files. + * + * `CakePlugin::load('DebugKit', array('bootstrap' => 'aCallableMethod'));` + * + * Will run the aCallableMethod function to initialize it. + * + * Bootstrap initialization functions can be expressed as a PHP callback type, + * including closures. Callbacks will receive two parameters + * (plugin name, plugin configuration). + * + * It is also possible to load multiple plugins at once. Examples: + * + * `CakePlugin::load(array('DebugKit', 'ApiGenerator'));` + * + * Will load the DebugKit and ApiGenerator plugins. + * + * `CakePlugin::load(array('DebugKit', 'ApiGenerator'), array('bootstrap' => true));` + * + * Will load bootstrap file for both plugins. + * + * ``` + * CakePlugin::load(array( + * 'DebugKit' => array('routes' => true), + * 'ApiGenerator' + * ), + * array('bootstrap' => true) + * ); + * ``` + * + * Will only load the bootstrap for ApiGenerator and only the routes for DebugKit. + * By using the `path` option you can specify an absolute path to the plugin. Make + * sure that the path is slash terminated or your plugin will not be located properly. + * + * @param string|array $plugin name of the plugin to be loaded in CamelCase format or array or plugins to load + * @param array $config configuration options for the plugin + * @return void + * @throws MissingPluginException if the folder for the plugin to be loaded is not found + */ + public static function load($plugin, $config = []) + { + if (is_array($plugin)) { + foreach ($plugin as $name => $conf) { + list($name, $conf) = (is_numeric($name)) ? [$conf, $config] : [$name, $conf]; + static::load($name, $conf); + } + return; + } + $config += ['bootstrap' => false, 'routes' => false, 'ignoreMissing' => false]; + if (empty($config['path'])) { + foreach (App::path('plugins') as $path) { + if (is_dir($path . $plugin)) { + static::$_plugins[$plugin] = $config + ['path' => $path . $plugin . DS]; + break; + } - if (empty(static::$_plugins[$plugin]['path'])) { - throw new MissingPluginException(array('plugin' => $plugin)); - } - if (!empty(static::$_plugins[$plugin]['bootstrap'])) { - static::bootstrap($plugin); - } - } + //Backwards compatibility to make easier to migrate to 2.0 + $underscored = Inflector::underscore($plugin); + if (is_dir($path . $underscored)) { + static::$_plugins[$plugin] = $config + ['path' => $path . $underscored . DS]; + break; + } + } + } else { + static::$_plugins[$plugin] = $config; + } -/** - * Will load all the plugins located in the configured plugins folders - * - * If passed an options array, it will be used as a common default for all plugins to be loaded - * It is possible to set specific defaults for each plugins in the options array. Examples: - * - * ``` - * CakePlugin::loadAll(array( - * array('bootstrap' => true), - * 'DebugKit' => array('routes' => true, 'bootstrap' => false), - * )); - * ``` - * - * The above example will load the bootstrap file for all plugins, but for DebugKit it will only load - * the routes file and will not look for any bootstrap script. If you are loading - * many plugins that inconsistently support routes/bootstrap files, instead of detailing - * each plugin you can use the `ignoreMissing` option: - * - * ``` - * CakePlugin::loadAll(array( - * 'ignoreMissing' => true, - * 'bootstrap' => true, - * 'routes' => true, - * )); - * ``` - * - * The ignoreMissing option will do additional file_exists() calls but is simpler - * to use. - * - * @param array $options Options list. See CakePlugin::load() for valid options. - * @return void - */ - public static function loadAll($options = array()) { - $plugins = App::objects('plugins'); - foreach ($plugins as $plugin) { - $pluginOptions = isset($options[$plugin]) ? (array)$options[$plugin] : array(); - if (isset($options[0])) { - $pluginOptions += $options[0]; - } - static::load($plugin, $pluginOptions); - } - } + if (empty(static::$_plugins[$plugin]['path'])) { + throw new MissingPluginException(['plugin' => $plugin]); + } + if (!empty(static::$_plugins[$plugin]['bootstrap'])) { + static::bootstrap($plugin); + } + } -/** - * Returns the filesystem path for a plugin - * - * @param string $plugin name of the plugin in CamelCase format - * @return string path to the plugin folder - * @throws MissingPluginException if the folder for plugin was not found or plugin has not been loaded - */ - public static function path($plugin) { - if (empty(static::$_plugins[$plugin])) { - throw new MissingPluginException(array('plugin' => $plugin)); - } - return static::$_plugins[$plugin]['path']; - } + /** + * Loads the bootstrapping files for a plugin, or calls the initialization setup in the configuration + * + * @param string $plugin name of the plugin + * @return mixed + * @see CakePlugin::load() for examples of bootstrap configuration + */ + public static function bootstrap($plugin) + { + $config = static::$_plugins[$plugin]; + if ($config['bootstrap'] === false) { + return false; + } + if (is_callable($config['bootstrap'])) { + return call_user_func_array($config['bootstrap'], [$plugin, $config]); + } -/** - * Loads the bootstrapping files for a plugin, or calls the initialization setup in the configuration - * - * @param string $plugin name of the plugin - * @return mixed - * @see CakePlugin::load() for examples of bootstrap configuration - */ - public static function bootstrap($plugin) { - $config = static::$_plugins[$plugin]; - if ($config['bootstrap'] === false) { - return false; - } - if (is_callable($config['bootstrap'])) { - return call_user_func_array($config['bootstrap'], array($plugin, $config)); - } + $path = static::path($plugin); + if ($config['bootstrap'] === true) { + return static::_includeFile( + $path . 'Config' . DS . 'bootstrap.php', + $config['ignoreMissing'] + ); + } - $path = static::path($plugin); - if ($config['bootstrap'] === true) { - return static::_includeFile( - $path . 'Config' . DS . 'bootstrap.php', - $config['ignoreMissing'] - ); - } + $bootstrap = (array)$config['bootstrap']; + foreach ($bootstrap as $file) { + static::_includeFile( + $path . 'Config' . DS . $file . '.php', + $config['ignoreMissing'] + ); + } - $bootstrap = (array)$config['bootstrap']; - foreach ($bootstrap as $file) { - static::_includeFile( - $path . 'Config' . DS . $file . '.php', - $config['ignoreMissing'] - ); - } + return true; + } - return true; - } + /** + * Returns the filesystem path for a plugin + * + * @param string $plugin name of the plugin in CamelCase format + * @return string path to the plugin folder + * @throws MissingPluginException if the folder for plugin was not found or plugin has not been loaded + */ + public static function path($plugin) + { + if (empty(static::$_plugins[$plugin])) { + throw new MissingPluginException(['plugin' => $plugin]); + } + return static::$_plugins[$plugin]['path']; + } -/** - * Loads the routes file for a plugin, or all plugins configured to load their respective routes file - * - * @param string $plugin name of the plugin, if null will operate on all plugins having enabled the - * loading of routes files - * @return bool - */ - public static function routes($plugin = null) { - if ($plugin === null) { - foreach (static::loaded() as $p) { - static::routes($p); - } - return true; - } - $config = static::$_plugins[$plugin]; - if ($config['routes'] === false) { - return false; - } - return (bool)static::_includeFile( - static::path($plugin) . 'Config' . DS . 'routes.php', - $config['ignoreMissing'] - ); - } + /** + * Include file, ignoring include error if needed if file is missing + * + * @param string $file File to include + * @param bool $ignoreMissing Whether to ignore include error for missing files + * @return mixed + */ + protected static function _includeFile($file, $ignoreMissing = false) + { + if ($ignoreMissing && !is_file($file)) { + return false; + } + return include $file; + } -/** - * Returns true if the plugin $plugin is already loaded - * If plugin is null, it will return a list of all loaded plugins - * - * @param string $plugin Plugin name to check. - * @return mixed boolean true if $plugin is already loaded. - * If $plugin is null, returns a list of plugins that have been loaded - */ - public static function loaded($plugin = null) { - if ($plugin) { - return isset(static::$_plugins[$plugin]); - } - $return = array_keys(static::$_plugins); - sort($return); - return $return; - } + /** + * Loads the routes file for a plugin, or all plugins configured to load their respective routes file + * + * @param string $plugin name of the plugin, if null will operate on all plugins having enabled the + * loading of routes files + * @return bool + */ + public static function routes($plugin = null) + { + if ($plugin === null) { + foreach (static::loaded() as $p) { + static::routes($p); + } + return true; + } + $config = static::$_plugins[$plugin]; + if ($config['routes'] === false) { + return false; + } + return (bool)static::_includeFile( + static::path($plugin) . 'Config' . DS . 'routes.php', + $config['ignoreMissing'] + ); + } -/** - * Forgets a loaded plugin or all of them if first parameter is null - * - * @param string $plugin name of the plugin to forget - * @return void - */ - public static function unload($plugin = null) { - if ($plugin === null) { - static::$_plugins = array(); - } else { - unset(static::$_plugins[$plugin]); - } - } + /** + * Returns true if the plugin $plugin is already loaded + * If plugin is null, it will return a list of all loaded plugins + * + * @param string $plugin Plugin name to check. + * @return mixed boolean true if $plugin is already loaded. + * If $plugin is null, returns a list of plugins that have been loaded + */ + public static function loaded($plugin = null) + { + if ($plugin) { + return isset(static::$_plugins[$plugin]); + } + $return = array_keys(static::$_plugins); + sort($return); + return $return; + } -/** - * Include file, ignoring include error if needed if file is missing - * - * @param string $file File to include - * @param bool $ignoreMissing Whether to ignore include error for missing files - * @return mixed - */ - protected static function _includeFile($file, $ignoreMissing = false) { - if ($ignoreMissing && !is_file($file)) { - return false; - } - return include $file; - } + /** + * Forgets a loaded plugin or all of them if first parameter is null + * + * @param string $plugin name of the plugin to forget + * @return void + */ + public static function unload($plugin = null) + { + if ($plugin === null) { + static::$_plugins = []; + } else { + unset(static::$_plugins[$plugin]); + } + } } diff --git a/lib/Cake/Core/Configure.php b/lib/Cake/Core/Configure.php index 901606cd..b2376658 100755 --- a/lib/Cake/Core/Configure.php +++ b/lib/Cake/Core/Configure.php @@ -32,452 +32,471 @@ * @package Cake.Core * @link https://book.cakephp.org/2.0/en/development/configuration.html#configure-class */ -class Configure { - -/** - * Array of values currently stored in Configure. - * - * @var array - */ - protected static $_values = array( - 'debug' => 0 - ); - -/** - * Configured reader classes, used to load config files from resources - * - * @var array - * @see Configure::load() - */ - protected static $_readers = array(); - -/** - * Initializes configure and runs the bootstrap process. - * Bootstrapping includes the following steps: - * - * - Setup App array in Configure. - * - Include app/Config/core.php. - * - Configure core cache configurations. - * - Load App cache files. - * - Include app/Config/bootstrap.php. - * - Setup error/exception handlers. - * - * @param bool $boot Whether to do bootstrapping. - * @return void - */ - public static function bootstrap($boot = true) { - if ($boot) { - static::_appDefaults(); - - if (!include CONFIG . 'core.php') { - trigger_error(__d('cake_dev', - "Can't find application core file. Please create %s, and make sure it is readable by PHP.", - CONFIG . 'core.php'), - E_USER_ERROR - ); - } - App::init(); - App::$bootstrapping = false; - App::build(); - - $exception = array( - 'handler' => 'ErrorHandler::handleException', - ); - $error = array( - 'handler' => 'ErrorHandler::handleError', - 'level' => E_ALL & ~E_DEPRECATED, - ); - if (PHP_SAPI === 'cli') { - App::uses('ConsoleErrorHandler', 'Console'); - $console = new ConsoleErrorHandler(); - $exception['handler'] = array($console, 'handleException'); - $error['handler'] = array($console, 'handleError'); - } - static::_setErrorHandlers($error, $exception); - - if (!include CONFIG . 'bootstrap.php') { - trigger_error(__d('cake_dev', - "Can't find application bootstrap file. Please create %s, and make sure it is readable by PHP.", - CONFIG . 'bootstrap.php'), - E_USER_ERROR - ); - } - restore_error_handler(); - - static::_setErrorHandlers( - static::$_values['Error'], - static::$_values['Exception'] - ); - - // Preload Debugger + CakeText in case of E_STRICT errors when loading files. - if (static::$_values['debug'] > 0) { - class_exists('Debugger'); - class_exists('CakeText'); - } - if (!defined('TESTS')) { - define('TESTS', APP . 'Test' . DS); - } - } - } - -/** - * Set app's default configs - * - * @return void - */ - protected static function _appDefaults() { - static::write('App', (array)static::read('App') + array( - 'base' => false, - 'baseUrl' => false, - 'dir' => APP_DIR, - 'webroot' => WEBROOT_DIR, - 'www_root' => WWW_ROOT - )); - } - -/** - * Used to store a dynamic variable in Configure. - * - * Usage: - * ``` - * Configure::write('One.key1', 'value of the Configure::One[key1]'); - * Configure::write(array('One.key1' => 'value of the Configure::One[key1]')); - * Configure::write('One', array( - * 'key1' => 'value of the Configure::One[key1]', - * 'key2' => 'value of the Configure::One[key2]' - * ); - * - * Configure::write(array( - * 'One.key1' => 'value of the Configure::One[key1]', - * 'One.key2' => 'value of the Configure::One[key2]' - * )); - * ``` - * - * @param string|array $config The key to write, can be a dot notation value. - * Alternatively can be an array containing key(s) and value(s). - * @param mixed $value Value to set for var - * @return bool True if write was successful - * @link https://book.cakephp.org/2.0/en/development/configuration.html#Configure::write - */ - public static function write($config, $value = null) { - if (!is_array($config)) { - $config = array($config => $value); - } - - foreach ($config as $name => $value) { - static::$_values = Hash::insert(static::$_values, $name, $value); - } - - if (isset($config['debug']) && function_exists('ini_set')) { - if (static::$_values['debug']) { - ini_set('display_errors', 1); - } else { - ini_set('display_errors', 0); - } - } - return true; - } - -/** - * Used to read information stored in Configure. It's not - * possible to store `null` values in Configure. - * - * Usage: - * ``` - * Configure::read('Name'); will return all values for Name - * Configure::read('Name.key'); will return only the value of Configure::Name[key] - * ``` - * - * @param string|null $var Variable to obtain. Use '.' to access array elements. - * @return mixed value stored in configure, or null. - * @link https://book.cakephp.org/2.0/en/development/configuration.html#Configure::read - */ - public static function read($var = null) { - if ($var === null) { - return static::$_values; - } - return Hash::get(static::$_values, $var); - } - -/** - * Used to read and delete a variable from Configure. - * - * This is primarily used during bootstrapping to move configuration data - * out of configure into the various other classes in CakePHP. - * - * @param string $var The key to read and remove. - * @return array|null - */ - public static function consume($var) { - $simple = strpos($var, '.') === false; - if ($simple && !isset(static::$_values[$var])) { - return null; - } - if ($simple) { - $value = static::$_values[$var]; - unset(static::$_values[$var]); - return $value; - } - $value = Hash::get(static::$_values, $var); - static::$_values = Hash::remove(static::$_values, $var); - return $value; - } - -/** - * Returns true if given variable is set in Configure. - * - * @param string $var Variable name to check for - * @return bool True if variable is there - */ - public static function check($var) { - if (empty($var)) { - return false; - } - return Hash::get(static::$_values, $var) !== null; - } - -/** - * Used to delete a variable from Configure. - * - * Usage: - * ``` - * Configure::delete('Name'); will delete the entire Configure::Name - * Configure::delete('Name.key'); will delete only the Configure::Name[key] - * ``` - * - * @param string $var the var to be deleted - * @return void - * @link https://book.cakephp.org/2.0/en/development/configuration.html#Configure::delete - */ - public static function delete($var) { - static::$_values = Hash::remove(static::$_values, $var); - } - -/** - * Add a new reader to Configure. Readers allow you to read configuration - * files in various formats/storage locations. CakePHP comes with two built-in readers - * PhpReader and IniReader. You can also implement your own reader classes in your application. - * - * To add a new reader to Configure: - * - * `Configure::config('ini', new IniReader());` - * - * @param string $name The name of the reader being configured. This alias is used later to - * read values from a specific reader. - * @param ConfigReaderInterface $reader The reader to append. - * @return void - */ - public static function config($name, ConfigReaderInterface $reader) { - static::$_readers[$name] = $reader; - } - -/** - * Gets the names of the configured reader objects. - * - * @param string|null $name Name to check. If null returns all configured reader names. - * @return array Array of the configured reader objects. - */ - public static function configured($name = null) { - if ($name) { - return isset(static::$_readers[$name]); - } - return array_keys(static::$_readers); - } - -/** - * Remove a configured reader. This will unset the reader - * and make any future attempts to use it cause an Exception. - * - * @param string $name Name of the reader to drop. - * @return bool Success - */ - public static function drop($name) { - if (!isset(static::$_readers[$name])) { - return false; - } - unset(static::$_readers[$name]); - return true; - } - -/** - * Loads stored configuration information from a resource. You can add - * config file resource readers with `Configure::config()`. - * - * Loaded configuration information will be merged with the current - * runtime configuration. You can load configuration files from plugins - * by preceding the filename with the plugin name. - * - * `Configure::load('Users.user', 'default')` - * - * Would load the 'user' config file using the default config reader. You can load - * app config files by giving the name of the resource you want loaded. - * - * `Configure::load('setup', 'default');` - * - * If using `default` config and no reader has been configured for it yet, - * one will be automatically created using PhpReader - * - * @param string $key name of configuration resource to load. - * @param string $config Name of the configured reader to use to read the resource identified by $key. - * @param bool $merge if config files should be merged instead of simply overridden - * @return bool False if file not found, true if load successful. - * @throws ConfigureException Will throw any exceptions the reader raises. - * @link https://book.cakephp.org/2.0/en/development/configuration.html#Configure::load - */ - public static function load($key, $config = 'default', $merge = true) { - $reader = static::_getReader($config); - if (!$reader) { - return false; - } - $values = $reader->read($key); - - if ($merge) { - $keys = array_keys($values); - foreach ($keys as $key) { - if (($c = static::read($key)) && is_array($values[$key]) && is_array($c)) { - $values[$key] = Hash::merge($c, $values[$key]); - } - } - } - - return static::write($values); - } - -/** - * Dump data currently in Configure into $key. The serialization format - * is decided by the config reader attached as $config. For example, if the - * 'default' adapter is a PhpReader, the generated file will be a PHP - * configuration file loadable by the PhpReader. - * - * ## Usage - * - * Given that the 'default' reader is an instance of PhpReader. - * Save all data in Configure to the file `my_config.php`: - * - * `Configure::dump('my_config.php', 'default');` - * - * Save only the error handling configuration: - * - * `Configure::dump('error.php', 'default', array('Error', 'Exception');` - * - * @param string $key The identifier to create in the config adapter. - * This could be a filename or a cache key depending on the adapter being used. - * @param string $config The name of the configured adapter to dump data with. - * @param array $keys The name of the top-level keys you want to dump. - * This allows you save only some data stored in Configure. - * @return bool success - * @throws ConfigureException if the adapter does not implement a `dump` method. - */ - public static function dump($key, $config = 'default', $keys = array()) { - $reader = static::_getReader($config); - if (!$reader) { - throw new ConfigureException(__d('cake_dev', 'There is no "%s" adapter.', $config)); - } - if (!method_exists($reader, 'dump')) { - throw new ConfigureException(__d('cake_dev', 'The "%s" adapter, does not have a %s method.', $config, 'dump()')); - } - $values = static::$_values; - if (!empty($keys) && is_array($keys)) { - $values = array_intersect_key($values, array_flip($keys)); - } - return (bool)$reader->dump($key, $values); - } - -/** - * Get the configured reader. Internally used by `Configure::load()` and `Configure::dump()` - * Will create new PhpReader for default if not configured yet. - * - * @param string $config The name of the configured adapter - * @return mixed Reader instance or false - */ - protected static function _getReader($config) { - if (!isset(static::$_readers[$config])) { - if ($config !== 'default') { - return false; - } - App::uses('PhpReader', 'Configure'); - static::config($config, new PhpReader()); - } - return static::$_readers[$config]; - } - -/** - * Used to determine the current version of CakePHP. - * - * Usage `Configure::version();` - * - * @return string Current version of CakePHP - */ - public static function version() { - if (!isset(static::$_values['Cake']['version'])) { - require CAKE . 'Config' . DS . 'config.php'; - static::write($config); - } - return static::$_values['Cake']['version']; - } - -/** - * Used to write runtime configuration into Cache. Stored runtime configuration can be - * restored using `Configure::restore()`. These methods can be used to enable configuration managers - * frontends, or other GUI type interfaces for configuration. - * - * @param string $name The storage name for the saved configuration. - * @param string $cacheConfig The cache configuration to save into. Defaults to 'default' - * @param array $data Either an array of data to store, or leave empty to store all values. - * @return bool Success - */ - public static function store($name, $cacheConfig = 'default', $data = null) { - if ($data === null) { - $data = static::$_values; - } - return Cache::write($name, $data, $cacheConfig); - } - -/** - * Restores configuration data stored in the Cache into configure. Restored - * values will overwrite existing ones. - * - * @param string $name Name of the stored config file to load. - * @param string $cacheConfig Name of the Cache configuration to read from. - * @return bool Success. - */ - public static function restore($name, $cacheConfig = 'default') { - $values = Cache::read($name, $cacheConfig); - if ($values) { - return static::write($values); - } - return false; - } - -/** - * Clear all values stored in Configure. - * - * @return bool Success. - */ - public static function clear() { - static::$_values = array(); - return true; - } - -/** - * Set the error and exception handlers. - * - * @param array $error The Error handling configuration. - * @param array $exception The exception handling configuration. - * @return void - */ - protected static function _setErrorHandlers($error, $exception) { - $level = -1; - if (isset($error['level'])) { - error_reporting($error['level']); - $level = $error['level']; - } - if (!empty($error['handler'])) { - set_error_handler($error['handler'], $level); - } - if (!empty($exception['handler'])) { - set_exception_handler($exception['handler']); - } - } +class Configure +{ + + /** + * Array of values currently stored in Configure. + * + * @var array + */ + protected static $_values = [ + 'debug' => 0 + ]; + + /** + * Configured reader classes, used to load config files from resources + * + * @var array + * @see Configure::load() + */ + protected static $_readers = []; + + /** + * Initializes configure and runs the bootstrap process. + * Bootstrapping includes the following steps: + * + * - Setup App array in Configure. + * - Include app/Config/core.php. + * - Configure core cache configurations. + * - Load App cache files. + * - Include app/Config/bootstrap.php. + * - Setup error/exception handlers. + * + * @param bool $boot Whether to do bootstrapping. + * @return void + */ + public static function bootstrap($boot = true) + { + if ($boot) { + static::_appDefaults(); + + if (!include CONFIG . 'core.php') { + trigger_error(__d('cake_dev', + "Can't find application core file. Please create %s, and make sure it is readable by PHP.", + CONFIG . 'core.php'), + E_USER_ERROR + ); + } + App::init(); + App::$bootstrapping = false; + App::build(); + + $exception = [ + 'handler' => 'ErrorHandler::handleException', + ]; + $error = [ + 'handler' => 'ErrorHandler::handleError', + 'level' => E_ALL & ~E_DEPRECATED, + ]; + if (PHP_SAPI === 'cli') { + App::uses('ConsoleErrorHandler', 'Console'); + $console = new ConsoleErrorHandler(); + $exception['handler'] = [$console, 'handleException']; + $error['handler'] = [$console, 'handleError']; + } + static::_setErrorHandlers($error, $exception); + + if (!include CONFIG . 'bootstrap.php') { + trigger_error(__d('cake_dev', + "Can't find application bootstrap file. Please create %s, and make sure it is readable by PHP.", + CONFIG . 'bootstrap.php'), + E_USER_ERROR + ); + } + restore_error_handler(); + + static::_setErrorHandlers( + static::$_values['Error'], + static::$_values['Exception'] + ); + + // Preload Debugger + CakeText in case of E_STRICT errors when loading files. + if (static::$_values['debug'] > 0) { + class_exists('Debugger'); + class_exists('CakeText'); + } + if (!defined('TESTS')) { + define('TESTS', APP . 'Test' . DS); + } + } + } + + /** + * Set app's default configs + * + * @return void + */ + protected static function _appDefaults() + { + static::write('App', (array)static::read('App') + [ + 'base' => false, + 'baseUrl' => false, + 'dir' => APP_DIR, + 'webroot' => WEBROOT_DIR, + 'www_root' => WWW_ROOT + ]); + } + + /** + * Used to store a dynamic variable in Configure. + * + * Usage: + * ``` + * Configure::write('One.key1', 'value of the Configure::One[key1]'); + * Configure::write(array('One.key1' => 'value of the Configure::One[key1]')); + * Configure::write('One', array( + * 'key1' => 'value of the Configure::One[key1]', + * 'key2' => 'value of the Configure::One[key2]' + * ); + * + * Configure::write(array( + * 'One.key1' => 'value of the Configure::One[key1]', + * 'One.key2' => 'value of the Configure::One[key2]' + * )); + * ``` + * + * @param string|array $config The key to write, can be a dot notation value. + * Alternatively can be an array containing key(s) and value(s). + * @param mixed $value Value to set for var + * @return bool True if write was successful + * @link https://book.cakephp.org/2.0/en/development/configuration.html#Configure::write + */ + public static function write($config, $value = null) + { + if (!is_array($config)) { + $config = [$config => $value]; + } + + foreach ($config as $name => $value) { + static::$_values = Hash::insert(static::$_values, $name, $value); + } + + if (isset($config['debug']) && function_exists('ini_set')) { + if (static::$_values['debug']) { + ini_set('display_errors', 1); + } else { + ini_set('display_errors', 0); + } + } + return true; + } + + /** + * Used to read information stored in Configure. It's not + * possible to store `null` values in Configure. + * + * Usage: + * ``` + * Configure::read('Name'); will return all values for Name + * Configure::read('Name.key'); will return only the value of Configure::Name[key] + * ``` + * + * @param string|null $var Variable to obtain. Use '.' to access array elements. + * @return mixed value stored in configure, or null. + * @link https://book.cakephp.org/2.0/en/development/configuration.html#Configure::read + */ + public static function read($var = null) + { + if ($var === null) { + return static::$_values; + } + return Hash::get(static::$_values, $var); + } + + /** + * Set the error and exception handlers. + * + * @param array $error The Error handling configuration. + * @param array $exception The exception handling configuration. + * @return void + */ + protected static function _setErrorHandlers($error, $exception) + { + $level = -1; + if (isset($error['level'])) { + error_reporting($error['level']); + $level = $error['level']; + } + if (!empty($error['handler'])) { + set_error_handler($error['handler'], $level); + } + if (!empty($exception['handler'])) { + set_exception_handler($exception['handler']); + } + } + + /** + * Used to read and delete a variable from Configure. + * + * This is primarily used during bootstrapping to move configuration data + * out of configure into the various other classes in CakePHP. + * + * @param string $var The key to read and remove. + * @return array|null + */ + public static function consume($var) + { + $simple = strpos($var, '.') === false; + if ($simple && !isset(static::$_values[$var])) { + return null; + } + if ($simple) { + $value = static::$_values[$var]; + unset(static::$_values[$var]); + return $value; + } + $value = Hash::get(static::$_values, $var); + static::$_values = Hash::remove(static::$_values, $var); + return $value; + } + + /** + * Returns true if given variable is set in Configure. + * + * @param string $var Variable name to check for + * @return bool True if variable is there + */ + public static function check($var) + { + if (empty($var)) { + return false; + } + return Hash::get(static::$_values, $var) !== null; + } + + /** + * Used to delete a variable from Configure. + * + * Usage: + * ``` + * Configure::delete('Name'); will delete the entire Configure::Name + * Configure::delete('Name.key'); will delete only the Configure::Name[key] + * ``` + * + * @param string $var the var to be deleted + * @return void + * @link https://book.cakephp.org/2.0/en/development/configuration.html#Configure::delete + */ + public static function delete($var) + { + static::$_values = Hash::remove(static::$_values, $var); + } + + /** + * Gets the names of the configured reader objects. + * + * @param string|null $name Name to check. If null returns all configured reader names. + * @return array Array of the configured reader objects. + */ + public static function configured($name = null) + { + if ($name) { + return isset(static::$_readers[$name]); + } + return array_keys(static::$_readers); + } + + /** + * Remove a configured reader. This will unset the reader + * and make any future attempts to use it cause an Exception. + * + * @param string $name Name of the reader to drop. + * @return bool Success + */ + public static function drop($name) + { + if (!isset(static::$_readers[$name])) { + return false; + } + unset(static::$_readers[$name]); + return true; + } + + /** + * Loads stored configuration information from a resource. You can add + * config file resource readers with `Configure::config()`. + * + * Loaded configuration information will be merged with the current + * runtime configuration. You can load configuration files from plugins + * by preceding the filename with the plugin name. + * + * `Configure::load('Users.user', 'default')` + * + * Would load the 'user' config file using the default config reader. You can load + * app config files by giving the name of the resource you want loaded. + * + * `Configure::load('setup', 'default');` + * + * If using `default` config and no reader has been configured for it yet, + * one will be automatically created using PhpReader + * + * @param string $key name of configuration resource to load. + * @param string $config Name of the configured reader to use to read the resource identified by $key. + * @param bool $merge if config files should be merged instead of simply overridden + * @return bool False if file not found, true if load successful. + * @throws ConfigureException Will throw any exceptions the reader raises. + * @link https://book.cakephp.org/2.0/en/development/configuration.html#Configure::load + */ + public static function load($key, $config = 'default', $merge = true) + { + $reader = static::_getReader($config); + if (!$reader) { + return false; + } + $values = $reader->read($key); + + if ($merge) { + $keys = array_keys($values); + foreach ($keys as $key) { + if (($c = static::read($key)) && is_array($values[$key]) && is_array($c)) { + $values[$key] = Hash::merge($c, $values[$key]); + } + } + } + + return static::write($values); + } + + /** + * Get the configured reader. Internally used by `Configure::load()` and `Configure::dump()` + * Will create new PhpReader for default if not configured yet. + * + * @param string $config The name of the configured adapter + * @return mixed Reader instance or false + */ + protected static function _getReader($config) + { + if (!isset(static::$_readers[$config])) { + if ($config !== 'default') { + return false; + } + App::uses('PhpReader', 'Configure'); + static::config($config, new PhpReader()); + } + return static::$_readers[$config]; + } + + /** + * Add a new reader to Configure. Readers allow you to read configuration + * files in various formats/storage locations. CakePHP comes with two built-in readers + * PhpReader and IniReader. You can also implement your own reader classes in your application. + * + * To add a new reader to Configure: + * + * `Configure::config('ini', new IniReader());` + * + * @param string $name The name of the reader being configured. This alias is used later to + * read values from a specific reader. + * @param ConfigReaderInterface $reader The reader to append. + * @return void + */ + public static function config($name, ConfigReaderInterface $reader) + { + static::$_readers[$name] = $reader; + } + + /** + * Dump data currently in Configure into $key. The serialization format + * is decided by the config reader attached as $config. For example, if the + * 'default' adapter is a PhpReader, the generated file will be a PHP + * configuration file loadable by the PhpReader. + * + * ## Usage + * + * Given that the 'default' reader is an instance of PhpReader. + * Save all data in Configure to the file `my_config.php`: + * + * `Configure::dump('my_config.php', 'default');` + * + * Save only the error handling configuration: + * + * `Configure::dump('error.php', 'default', array('Error', 'Exception');` + * + * @param string $key The identifier to create in the config adapter. + * This could be a filename or a cache key depending on the adapter being used. + * @param string $config The name of the configured adapter to dump data with. + * @param array $keys The name of the top-level keys you want to dump. + * This allows you save only some data stored in Configure. + * @return bool success + * @throws ConfigureException if the adapter does not implement a `dump` method. + */ + public static function dump($key, $config = 'default', $keys = []) + { + $reader = static::_getReader($config); + if (!$reader) { + throw new ConfigureException(__d('cake_dev', 'There is no "%s" adapter.', $config)); + } + if (!method_exists($reader, 'dump')) { + throw new ConfigureException(__d('cake_dev', 'The "%s" adapter, does not have a %s method.', $config, 'dump()')); + } + $values = static::$_values; + if (!empty($keys) && is_array($keys)) { + $values = array_intersect_key($values, array_flip($keys)); + } + return (bool)$reader->dump($key, $values); + } + + /** + * Used to determine the current version of CakePHP. + * + * Usage `Configure::version();` + * + * @return string Current version of CakePHP + */ + public static function version() + { + if (!isset(static::$_values['Cake']['version'])) { + require CAKE . 'Config' . DS . 'config.php'; + static::write($config); + } + return static::$_values['Cake']['version']; + } + + /** + * Used to write runtime configuration into Cache. Stored runtime configuration can be + * restored using `Configure::restore()`. These methods can be used to enable configuration managers + * frontends, or other GUI type interfaces for configuration. + * + * @param string $name The storage name for the saved configuration. + * @param string $cacheConfig The cache configuration to save into. Defaults to 'default' + * @param array $data Either an array of data to store, or leave empty to store all values. + * @return bool Success + */ + public static function store($name, $cacheConfig = 'default', $data = null) + { + if ($data === null) { + $data = static::$_values; + } + return Cache::write($name, $data, $cacheConfig); + } + + /** + * Restores configuration data stored in the Cache into configure. Restored + * values will overwrite existing ones. + * + * @param string $name Name of the stored config file to load. + * @param string $cacheConfig Name of the Cache configuration to read from. + * @return bool Success. + */ + public static function restore($name, $cacheConfig = 'default') + { + $values = Cache::read($name, $cacheConfig); + if ($values) { + return static::write($values); + } + return false; + } + + /** + * Clear all values stored in Configure. + * + * @return bool Success. + */ + public static function clear() + { + static::$_values = []; + return true; + } } diff --git a/lib/Cake/Error/ErrorHandler.php b/lib/Cake/Error/ErrorHandler.php index 79b24360..d2fb3b7b 100755 --- a/lib/Cake/Error/ErrorHandler.php +++ b/lib/Cake/Error/ErrorHandler.php @@ -141,6 +141,29 @@ public static function handleException($exception) } } + /** + * Handles exception logging + * + * @param Exception|ParseError $exception The exception to render. + * @param array $config An array of configuration for logging. + * @return bool + */ + protected static function _log($exception, $config) + { + if (empty($config['log'])) { + return false; + } + + if (!empty($config['skipLog'])) { + foreach ((array)$config['skipLog'] as $class) { + if ($exception instanceof $class) { + return false; + } + } + } + return CakeLog::write(LOG_ERR, static::_getMessage($exception)); + } + /** * Generates a formatted error message * @@ -169,29 +192,6 @@ protected static function _getMessage($exception) return $message; } - /** - * Handles exception logging - * - * @param Exception|ParseError $exception The exception to render. - * @param array $config An array of configuration for logging. - * @return bool - */ - protected static function _log($exception, $config) - { - if (empty($config['log'])) { - return false; - } - - if (!empty($config['skipLog'])) { - foreach ((array)$config['skipLog'] as $class) { - if ($exception instanceof $class) { - return false; - } - } - } - return CakeLog::write(LOG_ERR, static::_getMessage($exception)); - } - /** * Set as the default error handler by CakePHP. Use Configure::write('Error.handler', $callback), to use your own * error handling methods. This function will use Debugger to display errors when debug > 0. And @@ -219,7 +219,7 @@ public static function handleError($code, $description, $file = null, $line = nu $debug = Configure::read('debug'); if ($debug) { - $data = array( + $data = [ 'level' => $log, 'code' => $code, 'error' => $error, @@ -229,54 +229,13 @@ public static function handleError($code, $description, $file = null, $line = nu 'context' => $context, 'start' => 2, 'path' => Debugger::trimPath($file) - ); + ]; return Debugger::getInstance()->outputError($data); } $message = static::_getErrorMessage($error, $code, $description, $file, $line); return CakeLog::write($log, $message); } - /** - * Generate an error page when some fatal error happens. - * - * @param int $code Code of error - * @param string $description Error description - * @param string $file File on which error occurred - * @param int $line Line that triggered the error - * @return bool - * @throws FatalErrorException If the Exception renderer threw an exception during rendering, and debug > 0. - * @throws InternalErrorException If the Exception renderer threw an exception during rendering, and debug is 0. - */ - public static function handleFatalError($code, $description, $file, $line) - { - $logMessage = 'Fatal Error (' . $code . '): ' . $description . ' in [' . $file . ', line ' . $line . ']'; - CakeLog::write(LOG_ERR, $logMessage); - - $exceptionHandler = Configure::read('Exception.handler'); - if (!is_callable($exceptionHandler)) { - return false; - } - - if (ob_get_level()) { - ob_end_clean(); - } - - if (Configure::read('debug')) { - $exception = new FatalErrorException($description, 500, $file, $line); - } else { - $exception = new InternalErrorException(); - } - - if (static::$_bailExceptionRendering) { - static::$_bailExceptionRendering = false; - throw $exception; - } - - call_user_func($exceptionHandler, $exception); - - return false; - } - /** * Map an error code into an Error word, and log location. * @@ -317,7 +276,48 @@ public static function mapErrorCode($code) $log = LOG_NOTICE; break; } - return array($error, $log); + return [$error, $log]; + } + + /** + * Generate an error page when some fatal error happens. + * + * @param int $code Code of error + * @param string $description Error description + * @param string $file File on which error occurred + * @param int $line Line that triggered the error + * @return bool + * @throws FatalErrorException If the Exception renderer threw an exception during rendering, and debug > 0. + * @throws InternalErrorException If the Exception renderer threw an exception during rendering, and debug is 0. + */ + public static function handleFatalError($code, $description, $file, $line) + { + $logMessage = 'Fatal Error (' . $code . '): ' . $description . ' in [' . $file . ', line ' . $line . ']'; + CakeLog::write(LOG_ERR, $logMessage); + + $exceptionHandler = Configure::read('Exception.handler'); + if (!is_callable($exceptionHandler)) { + return false; + } + + if (ob_get_level()) { + ob_end_clean(); + } + + if (Configure::read('debug')) { + $exception = new FatalErrorException($description, 500, $file, $line); + } else { + $exception = new InternalErrorException(); + } + + if (static::$_bailExceptionRendering) { + static::$_bailExceptionRendering = false; + throw $exception; + } + + call_user_func($exceptionHandler, $exception); + + return false; } /** @@ -345,7 +345,7 @@ protected static function _getErrorMessage($error, $code, $description, $file, $ App::load('CakeText'); } } - $trace = Debugger::trace(array('start' => 1, 'format' => 'log')); + $trace = Debugger::trace(['start' => 1, 'format' => 'log']); $message .= "\nTrace:\n" . $trace . "\n"; } return $message; diff --git a/lib/Cake/Error/ExceptionRenderer.php b/lib/Cake/Error/ExceptionRenderer.php index 99cf3d67..d699957d 100755 --- a/lib/Cake/Error/ExceptionRenderer.php +++ b/lib/Cake/Error/ExceptionRenderer.php @@ -121,11 +121,11 @@ public function __construct($exception) if (empty($template) || $template === 'internalError') { $template = 'error500'; } - } elseif ($exception instanceof PDOException) { + } else if ($exception instanceof PDOException) { $method = 'pdoError'; $template = 'pdo_error'; $code = 500; - } elseif (!$methodExists) { + } else if (!$methodExists) { $method = 'error500'; if ($code >= 400 && $code < 500) { $method = 'error400'; @@ -138,7 +138,7 @@ public function __construct($exception) } if ($isNotDebug && $code == 500 && get_class($exception) != "MissingConnectionException") { $method = 'error500'; - } elseif (get_class($exception) == "MissingConnectionException") { + } else if (get_class($exception) == "MissingConnectionException") { $method = 'missingConnection'; } $this->template = $template; @@ -204,7 +204,7 @@ protected function _getController($exception) public function render() { if ($this->method) { - call_user_func_array(array($this, $this->method), array($this->error)); + call_user_func_array([$this, $this->method], [$this->error]); } } @@ -216,16 +216,87 @@ public function forbidden($error) } $url = $this->controller->request->here(); $this->controller->response->statusCode(403); - $this->controller->set(array( + $this->controller->set([ 'name' => h($message), 'message' => h($message), 'url' => h($url), 'error' => $error, - '_serialize' => array('name', 'message', 'url') - )); + '_serialize' => ['name', 'message', 'url'] + ]); $this->_outputMessage('error403'); } + /** + * Generate the response using the controller object. + * + * @param string $template The template to render. + * @return void + */ + protected function _outputMessage($template) + { + try { + $this->controller->render($template); + $this->_shutdown(); + $this->controller->response->send(); + } catch (MissingViewException $e) { + $attributes = $e->getAttributes(); + if (isset($attributes['file']) && strpos($attributes['file'], 'error500') !== false) { + $this->_outputMessageSafe('error500'); + } else { + $this->_outputMessage('error500'); + } + } catch (MissingPluginException $e) { + $attributes = $e->getAttributes(); + if (isset($attributes['plugin']) && $attributes['plugin'] === $this->controller->plugin) { + $this->controller->plugin = null; + } + $this->_outputMessageSafe('error400'); + } catch (Exception $e) { + $this->_outputMessageSafe('error500'); + } + } + + /** + * Run the shutdown events. + * + * Triggers the afterFilter and afterDispatch events. + * + * @return void + */ + protected function _shutdown() + { + $afterFilterEvent = new CakeEvent('Controller.shutdown', $this->controller); + $this->controller->getEventManager()->dispatch($afterFilterEvent); + + $Dispatcher = new Dispatcher(); + $afterDispatchEvent = new CakeEvent('Dispatcher.afterDispatch', $Dispatcher, [ + 'request' => $this->controller->request, + 'response' => $this->controller->response + ]); + $Dispatcher->getEventManager()->dispatch($afterDispatchEvent); + } + + /** + * A safer way to render error messages, replaces all helpers, with basics + * and doesn't call component methods. + * + * @param string $template The template to render + * @return void + */ + protected function _outputMessageSafe($template) + { + $this->controller->layoutPath = null; + $this->controller->subDir = null; + $this->controller->viewPath = 'Errors'; + $this->controller->layout = 'error'; + $this->controller->helpers = ['Form', 'Html', 'Session']; + + $view = new View($this->controller); + $this->controller->response->body($view->render($template, 'error')); + $this->controller->response->type('html'); + $this->controller->response->send(); + } + public function notfound($error) { $message = $error->getMessage(); @@ -234,13 +305,13 @@ public function notfound($error) } $url = $this->controller->request->here(); $this->controller->response->statusCode(404); - $this->controller->set(array( + $this->controller->set([ 'name' => h($message), 'message' => h($message), 'url' => h($url), 'error' => $error, - '_serialize' => array('name', 'message', 'url') - )); + '_serialize' => ['name', 'message', 'url'] + ]); $this->_outputMessage('error404'); } @@ -252,39 +323,16 @@ function missingConnection($error) } $url = $this->controller->request->here(); $this->controller->response->statusCode(500); - $this->controller->set(array( + $this->controller->set([ 'name' => h($message), 'message' => h($message), 'url' => h($url), 'error' => $error, - '_serialize' => array('name', 'message', 'url') - )); + '_serialize' => ['name', 'message', 'url'] + ]); $this->_outputMessage('missing_connection'); } - /** - * Generic handler for the internal framework errors CakePHP can generate. - * - * @param CakeException $error The exception to render. - * @return void - */ - protected function _cakeError(CakeException $error) - { - $url = $this->controller->request->here(); - $code = ($error->getCode() >= 400 && $error->getCode() < 506) ? $error->getCode() : 500; - $this->controller->response->statusCode($code); - $this->controller->set(array( - 'code' => $code, - 'name' => h($error->getMessage()), - 'message' => h($error->getMessage()), - 'url' => h($url), - 'error' => $error, - '_serialize' => array('code', 'name', 'message', 'url') - )); - $this->controller->set($error->getAttributes()); - $this->_outputMessage($this->template); - } - /** * Convenience method to display a 400 series page. * @@ -299,13 +347,13 @@ public function error400($error) } $url = $this->controller->request->here(); $this->controller->response->statusCode($error->getCode()); - $this->controller->set(array( + $this->controller->set([ 'name' => h($message), 'message' => h($message), 'url' => h($url), 'error' => $error, - '_serialize' => array('name', 'message', 'url') - )); + '_serialize' => ['name', 'message', 'url'] + ]); $this->_outputMessage('error400'); } @@ -324,13 +372,13 @@ public function error500($error) $url = $this->controller->request->here(); $code = ($error->getCode() > 500 && $error->getCode() < 506) ? $error->getCode() : 500; $this->controller->response->statusCode($code); - $this->controller->set(array( + $this->controller->set([ 'name' => h($message), 'message' => h($message), 'url' => h($url), 'error' => $error, - '_serialize' => array('name', 'message', 'url') - )); + '_serialize' => ['name', 'message', 'url'] + ]); $this->_outputMessage('error500'); } @@ -345,86 +393,38 @@ public function pdoError(PDOException $error) $url = $this->controller->request->here(); $code = 500; $this->controller->response->statusCode($code); - $this->controller->set(array( + $this->controller->set([ 'code' => $code, 'name' => h($error->getMessage()), 'message' => h($error->getMessage()), 'url' => h($url), 'error' => $error, - '_serialize' => array('code', 'name', 'message', 'url', 'error') - )); + '_serialize' => ['code', 'name', 'message', 'url', 'error'] + ]); $this->_outputMessage($this->template); } /** - * Generate the response using the controller object. - * - * @param string $template The template to render. - * @return void - */ - protected function _outputMessage($template) - { - try { - $this->controller->render($template); - $this->_shutdown(); - $this->controller->response->send(); - } catch (MissingViewException $e) { - $attributes = $e->getAttributes(); - if (isset($attributes['file']) && strpos($attributes['file'], 'error500') !== false) { - $this->_outputMessageSafe('error500'); - } else { - $this->_outputMessage('error500'); - } - } catch (MissingPluginException $e) { - $attributes = $e->getAttributes(); - if (isset($attributes['plugin']) && $attributes['plugin'] === $this->controller->plugin) { - $this->controller->plugin = null; - } - $this->_outputMessageSafe('error400'); - } catch (Exception $e) { - $this->_outputMessageSafe('error500'); - } - } - - /** - * A safer way to render error messages, replaces all helpers, with basics - * and doesn't call component methods. - * - * @param string $template The template to render - * @return void - */ - protected function _outputMessageSafe($template) - { - $this->controller->layoutPath = null; - $this->controller->subDir = null; - $this->controller->viewPath = 'Errors'; - $this->controller->layout = 'error'; - $this->controller->helpers = array('Form', 'Html', 'Session'); - - $view = new View($this->controller); - $this->controller->response->body($view->render($template, 'error')); - $this->controller->response->type('html'); - $this->controller->response->send(); - } - - /** - * Run the shutdown events. - * - * Triggers the afterFilter and afterDispatch events. + * Generic handler for the internal framework errors CakePHP can generate. * + * @param CakeException $error The exception to render. * @return void */ - protected function _shutdown() + protected function _cakeError(CakeException $error) { - $afterFilterEvent = new CakeEvent('Controller.shutdown', $this->controller); - $this->controller->getEventManager()->dispatch($afterFilterEvent); - - $Dispatcher = new Dispatcher(); - $afterDispatchEvent = new CakeEvent('Dispatcher.afterDispatch', $Dispatcher, array( - 'request' => $this->controller->request, - 'response' => $this->controller->response - )); - $Dispatcher->getEventManager()->dispatch($afterDispatchEvent); + $url = $this->controller->request->here(); + $code = ($error->getCode() >= 400 && $error->getCode() < 506) ? $error->getCode() : 500; + $this->controller->response->statusCode($code); + $this->controller->set([ + 'code' => $code, + 'name' => h($error->getMessage()), + 'message' => h($error->getMessage()), + 'url' => h($url), + 'error' => $error, + '_serialize' => ['code', 'name', 'message', 'url'] + ]); + $this->controller->set($error->getAttributes()); + $this->_outputMessage($this->template); } } diff --git a/lib/Cake/Error/exceptions.php b/lib/Cake/Error/exceptions.php index 6608f622..742f1f9f 100755 --- a/lib/Cake/Error/exceptions.php +++ b/lib/Cake/Error/exceptions.php @@ -22,48 +22,51 @@ * * @package Cake.Error */ -class CakeBaseException extends RuntimeException { - -/** - * Array of headers to be passed to CakeResponse::header() - * - * @var array - */ - protected $_responseHeaders = null; - -/** - * Get/set the response header to be used - * - * @param string|array $header An array of header strings or a single header string - * - an associative array of "header name" => "header value" - * - an array of string headers is also accepted - * @param string $value The header value. - * @return array - * @see CakeResponse::header() - */ - public function responseHeader($header = null, $value = null) { - if ($header) { - if (is_array($header)) { - return $this->_responseHeaders = $header; - } - $this->_responseHeaders = array($header => $value); - } - return $this->_responseHeaders; - } +class CakeBaseException extends RuntimeException +{ + + /** + * Array of headers to be passed to CakeResponse::header() + * + * @var array + */ + protected $_responseHeaders = null; + + /** + * Get/set the response header to be used + * + * @param string|array $header An array of header strings or a single header string + * - an associative array of "header name" => "header value" + * - an array of string headers is also accepted + * @param string $value The header value. + * @return array + * @see CakeResponse::header() + */ + public function responseHeader($header = null, $value = null) + { + if ($header) { + if (is_array($header)) { + return $this->_responseHeaders = $header; + } + $this->_responseHeaders = [$header => $value]; + } + return $this->_responseHeaders; + } } if (!class_exists('HttpException', false)) { -/** - * Parent class for all of the HTTP related exceptions in CakePHP. - * - * All HTTP status/error related exceptions should extend this class so - * catch blocks can be specifically typed. - * - * @package Cake.Error - */ - class HttpException extends CakeBaseException { - } + /** + * Parent class for all of the HTTP related exceptions in CakePHP. + * + * All HTTP status/error related exceptions should extend this class so + * catch blocks can be specifically typed. + * + * @package Cake.Error + */ + class HttpException extends CakeBaseException + { + } } /** @@ -71,20 +74,22 @@ class HttpException extends CakeBaseException { * * @package Cake.Error */ -class BadRequestException extends HttpException { +class BadRequestException extends HttpException +{ -/** - * Constructor - * - * @param string $message If no message is given 'Bad Request' will be the message - * @param int $code Status code, defaults to 400 - */ - public function __construct($message = null, $code = 400) { - if (empty($message)) { - $message = 'Bad Request'; - } - parent::__construct($message, $code); - } + /** + * Constructor + * + * @param string $message If no message is given 'Bad Request' will be the message + * @param int $code Status code, defaults to 400 + */ + public function __construct($message = null, $code = 400) + { + if (empty($message)) { + $message = 'Bad Request'; + } + parent::__construct($message, $code); + } } @@ -93,20 +98,22 @@ public function __construct($message = null, $code = 400) { * * @package Cake.Error */ -class UnauthorizedException extends HttpException { +class UnauthorizedException extends HttpException +{ -/** - * Constructor - * - * @param string $message If no message is given 'Unauthorized' will be the message - * @param int $code Status code, defaults to 401 - */ - public function __construct($message = null, $code = 401) { - if (empty($message)) { - $message = 'Unauthorized'; - } - parent::__construct($message, $code); - } + /** + * Constructor + * + * @param string $message If no message is given 'Unauthorized' will be the message + * @param int $code Status code, defaults to 401 + */ + public function __construct($message = null, $code = 401) + { + if (empty($message)) { + $message = 'Unauthorized'; + } + parent::__construct($message, $code); + } } @@ -115,20 +122,22 @@ public function __construct($message = null, $code = 401) { * * @package Cake.Error */ -class ForbiddenException extends HttpException { +class ForbiddenException extends HttpException +{ -/** - * Constructor - * - * @param string $message If no message is given 'Forbidden' will be the message - * @param int $code Status code, defaults to 403 - */ - public function __construct($message = null, $code = 403) { - if (empty($message)) { - $message = 'Forbidden'; - } - parent::__construct($message, $code); - } + /** + * Constructor + * + * @param string $message If no message is given 'Forbidden' will be the message + * @param int $code Status code, defaults to 403 + */ + public function __construct($message = null, $code = 403) + { + if (empty($message)) { + $message = 'Forbidden'; + } + parent::__construct($message, $code); + } } @@ -137,20 +146,22 @@ public function __construct($message = null, $code = 403) { * * @package Cake.Error */ -class NotFoundException extends HttpException { +class NotFoundException extends HttpException +{ -/** - * Constructor - * - * @param string $message If no message is given 'Not Found' will be the message - * @param int $code Status code, defaults to 404 - */ - public function __construct($message = null, $code = 404) { - if (empty($message)) { - $message = 'Not Found'; - } - parent::__construct($message, $code); - } + /** + * Constructor + * + * @param string $message If no message is given 'Not Found' will be the message + * @param int $code Status code, defaults to 404 + */ + public function __construct($message = null, $code = 404) + { + if (empty($message)) { + $message = 'Not Found'; + } + parent::__construct($message, $code); + } } @@ -159,20 +170,22 @@ public function __construct($message = null, $code = 404) { * * @package Cake.Error */ -class MethodNotAllowedException extends HttpException { +class MethodNotAllowedException extends HttpException +{ -/** - * Constructor - * - * @param string $message If no message is given 'Method Not Allowed' will be the message - * @param int $code Status code, defaults to 405 - */ - public function __construct($message = null, $code = 405) { - if (empty($message)) { - $message = 'Method Not Allowed'; - } - parent::__construct($message, $code); - } + /** + * Constructor + * + * @param string $message If no message is given 'Method Not Allowed' will be the message + * @param int $code Status code, defaults to 405 + */ + public function __construct($message = null, $code = 405) + { + if (empty($message)) { + $message = 'Method Not Allowed'; + } + parent::__construct($message, $code); + } } @@ -181,20 +194,22 @@ public function __construct($message = null, $code = 405) { * * @package Cake.Error */ -class InternalErrorException extends HttpException { +class InternalErrorException extends HttpException +{ -/** - * Constructor - * - * @param string $message If no message is given 'Internal Server Error' will be the message - * @param int $code Status code, defaults to 500 - */ - public function __construct($message = null, $code = 500) { - if (empty($message)) { - $message = 'Internal Server Error'; - } - parent::__construct($message, $code); - } + /** + * Constructor + * + * @param string $message If no message is given 'Internal Server Error' will be the message + * @param int $code Status code, defaults to 500 + */ + public function __construct($message = null, $code = 500) + { + if (empty($message)) { + $message = 'Internal Server Error'; + } + parent::__construct($message, $code); + } } @@ -204,49 +219,52 @@ public function __construct($message = null, $code = 500) { * * @package Cake.Error */ -class CakeException extends CakeBaseException { - -/** - * Array of attributes that are passed in from the constructor, and - * made available in the view when a development error is displayed. - * - * @var array - */ - protected $_attributes = array(); - -/** - * Template string that has attributes sprintf()'ed into it. - * - * @var string - */ - protected $_messageTemplate = ''; - -/** - * Constructor. - * - * Allows you to create exceptions that are treated as framework errors and disabled - * when debug = 0. - * - * @param string|array $message Either the string of the error message, or an array of attributes - * that are made available in the view, and sprintf()'d into CakeException::$_messageTemplate - * @param int $code The code of the error, is also the HTTP status code for the error. - */ - public function __construct($message, $code = 500) { - if (is_array($message)) { - $this->_attributes = $message; - $message = __d('cake_dev', $this->_messageTemplate, $message); - } - parent::__construct($message, $code); - } - -/** - * Get the passed in attributes - * - * @return array - */ - public function getAttributes() { - return $this->_attributes; - } +class CakeException extends CakeBaseException +{ + + /** + * Array of attributes that are passed in from the constructor, and + * made available in the view when a development error is displayed. + * + * @var array + */ + protected $_attributes = []; + + /** + * Template string that has attributes sprintf()'ed into it. + * + * @var string + */ + protected $_messageTemplate = ''; + + /** + * Constructor. + * + * Allows you to create exceptions that are treated as framework errors and disabled + * when debug = 0. + * + * @param string|array $message Either the string of the error message, or an array of attributes + * that are made available in the view, and sprintf()'d into CakeException::$_messageTemplate + * @param int $code The code of the error, is also the HTTP status code for the error. + */ + public function __construct($message, $code = 500) + { + if (is_array($message)) { + $this->_attributes = $message; + $message = __d('cake_dev', $this->_messageTemplate, $message); + } + parent::__construct($message, $code); + } + + /** + * Get the passed in attributes + * + * @return array + */ + public function getAttributes() + { + return $this->_attributes; + } } @@ -256,14 +274,16 @@ public function getAttributes() { * * @package Cake.Error */ -class MissingControllerException extends CakeException { +class MissingControllerException extends CakeException +{ - protected $_messageTemplate = 'Controller class %s could not be found.'; + protected $_messageTemplate = 'Controller class %s could not be found.'; //@codingStandardsIgnoreStart - public function __construct($message, $code = 404) { - parent::__construct($message, $code); - } + public function __construct($message, $code = 404) + { + parent::__construct($message, $code); + } //@codingStandardsIgnoreEnd } @@ -274,14 +294,16 @@ public function __construct($message, $code = 404) { * * @package Cake.Error */ -class MissingActionException extends CakeException { +class MissingActionException extends CakeException +{ - protected $_messageTemplate = 'Action %s::%s() could not be found.'; + protected $_messageTemplate = 'Action %s::%s() could not be found.'; //@codingStandardsIgnoreStart - public function __construct($message, $code = 404) { - parent::__construct($message, $code); - } + public function __construct($message, $code = 404) + { + parent::__construct($message, $code); + } //@codingStandardsIgnoreEnd } @@ -292,14 +314,16 @@ public function __construct($message, $code = 404) { * * @package Cake.Error */ -class PrivateActionException extends CakeException { +class PrivateActionException extends CakeException +{ - protected $_messageTemplate = 'Private Action %s::%s() is not directly accessible.'; + protected $_messageTemplate = 'Private Action %s::%s() is not directly accessible.'; //@codingStandardsIgnoreStart - public function __construct($message, $code = 404, Exception $previous = null) { - parent::__construct($message, $code, $previous); - } + public function __construct($message, $code = 404, Exception $previous = null) + { + parent::__construct($message, $code, $previous); + } //@codingStandardsIgnoreEnd } @@ -309,9 +333,10 @@ public function __construct($message, $code = 404, Exception $previous = null) { * * @package Cake.Error */ -class MissingComponentException extends CakeException { +class MissingComponentException extends CakeException +{ - protected $_messageTemplate = 'Component class %s could not be found.'; + protected $_messageTemplate = 'Component class %s could not be found.'; } @@ -320,9 +345,10 @@ class MissingComponentException extends CakeException { * * @package Cake.Error */ -class MissingBehaviorException extends CakeException { +class MissingBehaviorException extends CakeException +{ - protected $_messageTemplate = 'Behavior class %s could not be found.'; + protected $_messageTemplate = 'Behavior class %s could not be found.'; } @@ -331,9 +357,10 @@ class MissingBehaviorException extends CakeException { * * @package Cake.Error */ -class MissingViewException extends CakeException { +class MissingViewException extends CakeException +{ - protected $_messageTemplate = 'View file "%s" is missing.'; + protected $_messageTemplate = 'View file "%s" is missing.'; } @@ -342,9 +369,10 @@ class MissingViewException extends CakeException { * * @package Cake.Error */ -class MissingLayoutException extends CakeException { +class MissingLayoutException extends CakeException +{ - protected $_messageTemplate = 'Layout file "%s" is missing.'; + protected $_messageTemplate = 'Layout file "%s" is missing.'; } @@ -353,9 +381,10 @@ class MissingLayoutException extends CakeException { * * @package Cake.Error */ -class MissingHelperException extends CakeException { +class MissingHelperException extends CakeException +{ - protected $_messageTemplate = 'Helper class %s could not be found.'; + protected $_messageTemplate = 'Helper class %s could not be found.'; } @@ -364,9 +393,10 @@ class MissingHelperException extends CakeException { * * @package Cake.Error */ -class MissingDatabaseException extends CakeException { +class MissingDatabaseException extends CakeException +{ - protected $_messageTemplate = 'Database connection "%s" could not be found.'; + protected $_messageTemplate = 'Database connection "%s" could not be found.'; } @@ -375,22 +405,24 @@ class MissingDatabaseException extends CakeException { * * @package Cake.Error */ -class MissingConnectionException extends CakeException { +class MissingConnectionException extends CakeException +{ - protected $_messageTemplate = 'Database connection "%s" is missing, or could not be created.'; + protected $_messageTemplate = 'Database connection "%s" is missing, or could not be created.'; -/** - * Constructor - * - * @param string|array $message The error message. - * @param int $code The error code. - */ - public function __construct($message, $code = 500) { - if (is_array($message)) { - $message += array('enabled' => true); - } - parent::__construct($message, $code); - } + /** + * Constructor + * + * @param string|array $message The error message. + * @param int $code The error code. + */ + public function __construct($message, $code = 500) + { + if (is_array($message)) { + $message += ['enabled' => true]; + } + parent::__construct($message, $code); + } } @@ -399,9 +431,10 @@ public function __construct($message, $code = 500) { * * @package Cake.Error */ -class MissingTaskException extends CakeException { +class MissingTaskException extends CakeException +{ - protected $_messageTemplate = 'Task class %s could not be found.'; + protected $_messageTemplate = 'Task class %s could not be found.'; } @@ -410,9 +443,10 @@ class MissingTaskException extends CakeException { * * @package Cake.Error */ -class MissingShellMethodException extends CakeException { +class MissingShellMethodException extends CakeException +{ - protected $_messageTemplate = "Unknown command %1\$s %2\$s.\nFor usage try `cake %1\$s --help`"; + protected $_messageTemplate = "Unknown command %1\$s %2\$s.\nFor usage try `cake %1\$s --help`"; } @@ -421,9 +455,10 @@ class MissingShellMethodException extends CakeException { * * @package Cake.Error */ -class MissingShellException extends CakeException { +class MissingShellException extends CakeException +{ - protected $_messageTemplate = 'Shell class %s could not be found.'; + protected $_messageTemplate = 'Shell class %s could not be found.'; } @@ -432,9 +467,10 @@ class MissingShellException extends CakeException { * * @package Cake.Error */ -class MissingDatasourceConfigException extends CakeException { +class MissingDatasourceConfigException extends CakeException +{ - protected $_messageTemplate = 'The datasource configuration "%s" was not found in database.php'; + protected $_messageTemplate = 'The datasource configuration "%s" was not found in database.php'; } @@ -443,9 +479,10 @@ class MissingDatasourceConfigException extends CakeException { * * @package Cake.Error */ -class MissingDatasourceException extends CakeException { +class MissingDatasourceException extends CakeException +{ - protected $_messageTemplate = 'Datasource class %s could not be found. %s'; + protected $_messageTemplate = 'Datasource class %s could not be found. %s'; } @@ -454,9 +491,10 @@ class MissingDatasourceException extends CakeException { * * @package Cake.Error */ -class MissingTableException extends CakeException { +class MissingTableException extends CakeException +{ - protected $_messageTemplate = 'Table %s for model %s was not found in datasource %s.'; + protected $_messageTemplate = 'Table %s for model %s was not found in datasource %s.'; } @@ -465,9 +503,10 @@ class MissingTableException extends CakeException { * * @package Cake.Error */ -class MissingModelException extends CakeException { +class MissingModelException extends CakeException +{ - protected $_messageTemplate = 'Model %s could not be found.'; + protected $_messageTemplate = 'Model %s could not be found.'; } @@ -476,9 +515,10 @@ class MissingModelException extends CakeException { * * @package Cake.Error */ -class MissingTestLoaderException extends CakeException { +class MissingTestLoaderException extends CakeException +{ - protected $_messageTemplate = 'Test loader %s could not be found.'; + protected $_messageTemplate = 'Test loader %s could not be found.'; } @@ -487,9 +527,10 @@ class MissingTestLoaderException extends CakeException { * * @package Cake.Error */ -class MissingPluginException extends CakeException { +class MissingPluginException extends CakeException +{ - protected $_messageTemplate = 'Plugin %s could not be found.'; + protected $_messageTemplate = 'Plugin %s could not be found.'; } @@ -498,9 +539,10 @@ class MissingPluginException extends CakeException { * * @package Cake.Error */ -class MissingDispatcherFilterException extends CakeException { +class MissingDispatcherFilterException extends CakeException +{ - protected $_messageTemplate = 'Dispatcher filter %s could not be found.'; + protected $_messageTemplate = 'Dispatcher filter %s could not be found.'; } @@ -509,7 +551,8 @@ class MissingDispatcherFilterException extends CakeException { * * @package Cake.Error */ -class AclException extends CakeException { +class AclException extends CakeException +{ } /** @@ -518,7 +561,8 @@ class AclException extends CakeException { * * @package Cake.Error */ -class CacheException extends CakeException { +class CacheException extends CakeException +{ } /** @@ -527,7 +571,8 @@ class CacheException extends CakeException { * * @package Cake.Error */ -class RouterException extends CakeException { +class RouterException extends CakeException +{ } /** @@ -536,7 +581,8 @@ class RouterException extends CakeException { * * @package Cake.Error */ -class CakeLogException extends CakeException { +class CakeLogException extends CakeException +{ } /** @@ -545,7 +591,8 @@ class CakeLogException extends CakeException { * * @package Cake.Error */ -class CakeSessionException extends CakeException { +class CakeSessionException extends CakeException +{ } /** @@ -554,7 +601,8 @@ class CakeSessionException extends CakeException { * * @package Cake.Error */ -class ConfigureException extends CakeException { +class ConfigureException extends CakeException +{ } /** @@ -563,7 +611,8 @@ class ConfigureException extends CakeException { * * @package Cake.Error */ -class SocketException extends CakeException { +class SocketException extends CakeException +{ } /** @@ -572,7 +621,8 @@ class SocketException extends CakeException { * * @package Cake.Error */ -class XmlException extends CakeException { +class XmlException extends CakeException +{ } /** @@ -581,7 +631,8 @@ class XmlException extends CakeException { * * @package Cake.Error */ -class ConsoleException extends CakeException { +class ConsoleException extends CakeException +{ } /** @@ -589,25 +640,27 @@ class ConsoleException extends CakeException { * * @package Cake.Error */ -class FatalErrorException extends CakeException { +class FatalErrorException extends CakeException +{ -/** - * Constructor - * - * @param string $message The error message. - * @param int $code The error code. - * @param string $file The file the error occurred in. - * @param int $line The line the error occurred on. - */ - public function __construct($message, $code = 500, $file = null, $line = null) { - parent::__construct($message, $code); - if ($file) { - $this->file = $file; - } - if ($line) { - $this->line = $line; - } - } + /** + * Constructor + * + * @param string $message The error message. + * @param int $code The error code. + * @param string $file The file the error occurred in. + * @param int $line The line the error occurred on. + */ + public function __construct($message, $code = 500, $file = null, $line = null) + { + parent::__construct($message, $code); + if ($file) { + $this->file = $file; + } + if ($line) { + $this->line = $line; + } + } } @@ -616,14 +669,16 @@ public function __construct($message, $code = 500, $file = null, $line = null) { * * @package Cake.Error */ -class NotImplementedException extends CakeException { +class NotImplementedException extends CakeException +{ - protected $_messageTemplate = '%s is not implemented.'; + protected $_messageTemplate = '%s is not implemented.'; //@codingStandardsIgnoreStart - public function __construct($message, $code = 501) { - parent::__construct($message, $code); - } + public function __construct($message, $code = 501) + { + parent::__construct($message, $code); + } //@codingStandardsIgnoreEnd } @@ -633,58 +688,63 @@ public function __construct($message, $code = 501) { * * @package Cake.Error */ -class SecurityException extends BadRequestException { - -/** - * Security Exception type - * @var string - */ - protected $_type = 'secure'; - -/** - * Reason for request blackhole - * - * @var string - */ - protected $_reason = null; - -/** - * Getter for type - * - * @return string - */ - public function getType() { - return $this->_type; - } - -/** - * Set Message - * - * @param string $message Exception message - * @return void - */ - public function setMessage($message) { - $this->message = $message; - } - -/** - * Set Reason - * - * @param string|null $reason Reason details - * @return void - */ - public function setReason($reason = null) { - $this->_reason = $reason; - } - -/** - * Get Reason - * - * @return string - */ - public function getReason() { - return $this->_reason; - } +class SecurityException extends BadRequestException +{ + + /** + * Security Exception type + * @var string + */ + protected $_type = 'secure'; + + /** + * Reason for request blackhole + * + * @var string + */ + protected $_reason = null; + + /** + * Getter for type + * + * @return string + */ + public function getType() + { + return $this->_type; + } + + /** + * Set Message + * + * @param string $message Exception message + * @return void + */ + public function setMessage($message) + { + $this->message = $message; + } + + /** + * Get Reason + * + * @return string + */ + public function getReason() + { + return $this->_reason; + } + + /** + * Set Reason + * + * @param string|null $reason Reason details + * @return void + */ + public function setReason($reason = null) + { + $this->_reason = $reason; + } } @@ -693,12 +753,13 @@ public function getReason() { * * @package Cake.Error */ -class AuthSecurityException extends SecurityException { +class AuthSecurityException extends SecurityException +{ -/** - * Security Exception type - * @var string - */ - protected $_type = 'auth'; + /** + * Security Exception type + * @var string + */ + protected $_type = 'auth'; } diff --git a/lib/Cake/Event/CakeEvent.php b/lib/Cake/Event/CakeEvent.php index 4a23756e..a4ec935d 100755 --- a/lib/Cake/Event/CakeEvent.php +++ b/lib/Cake/Event/CakeEvent.php @@ -7,10 +7,10 @@ * For full copyright and license information, please see the LICENSE.txt * Redistributions of files must retain the above copyright notice. * - * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org) - * @link https://cakephp.org CakePHP(tm) Project - * @package Cake.Observer - * @since CakePHP(tm) v 2.1 + * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org) + * @link https://cakephp.org CakePHP(tm) Project + * @package Cake.Observer + * @since CakePHP(tm) v 2.1 * @license https://opensource.org/licenses/mit-license.php MIT License */ @@ -21,109 +21,112 @@ * * @package Cake.Event */ -class CakeEvent { +class CakeEvent +{ -/** - * Name of the event - * - * @var string - */ - protected $_name = null; + /** + * Custom data for the method that receives the event + * + * @var mixed + */ + public $data = null; + /** + * Property used to retain the result value of the event listeners + * + * @var mixed + */ + public $result = null; + /** + * Name of the event + * + * @var string + */ + protected $_name = null; + /** + * The object this event applies to (usually the same object that generates the event) + * + * @var object + */ + protected $_subject; + /** + * Flags an event as stopped or not, default is false + * + * @var bool + */ + protected $_stopped = false; -/** - * The object this event applies to (usually the same object that generates the event) - * - * @var object - */ - protected $_subject; + /** + * Constructor + * + * @param string $name Name of the event + * @param object $subject the object that this event applies to (usually the object that is generating the event) + * @param mixed $data any value you wish to be transported with this event to it can be read by listeners + * + * ## Examples of usage: + * + * ``` + * $event = new CakeEvent('Order.afterBuy', $this, array('buyer' => $userData)); + * $event = new CakeEvent('User.afterRegister', $UserModel); + * ``` + */ + public function __construct($name, $subject = null, $data = null) + { + $this->_name = $name; + $this->data = $data; + $this->_subject = $subject; + } -/** - * Custom data for the method that receives the event - * - * @var mixed - */ - public $data = null; + /** + * Dynamically returns the name and subject if accessed directly + * + * @param string $attribute Attribute name. + * @return mixed + */ + public function __get($attribute) + { + if ($attribute === 'name' || $attribute === 'subject') { + return $this->{$attribute}(); + } + } -/** - * Property used to retain the result value of the event listeners - * - * @var mixed - */ - public $result = null; + /** + * Returns the name of this event. This is usually used as the event identifier + * + * @return string + */ + public function name() + { + return $this->_name; + } -/** - * Flags an event as stopped or not, default is false - * - * @var bool - */ - protected $_stopped = false; + /** + * Returns the subject of this event + * + * @return object + */ + public function subject() + { + return $this->_subject; + } -/** - * Constructor - * - * @param string $name Name of the event - * @param object $subject the object that this event applies to (usually the object that is generating the event) - * @param mixed $data any value you wish to be transported with this event to it can be read by listeners - * - * ## Examples of usage: - * - * ``` - * $event = new CakeEvent('Order.afterBuy', $this, array('buyer' => $userData)); - * $event = new CakeEvent('User.afterRegister', $UserModel); - * ``` - */ - public function __construct($name, $subject = null, $data = null) { - $this->_name = $name; - $this->data = $data; - $this->_subject = $subject; - } + /** + * Stops the event from being used anymore + * + * @return bool + */ + public function stopPropagation() + { + return $this->_stopped = true; + } -/** - * Dynamically returns the name and subject if accessed directly - * - * @param string $attribute Attribute name. - * @return mixed - */ - public function __get($attribute) { - if ($attribute === 'name' || $attribute === 'subject') { - return $this->{$attribute}(); - } - } - -/** - * Returns the name of this event. This is usually used as the event identifier - * - * @return string - */ - public function name() { - return $this->_name; - } - -/** - * Returns the subject of this event - * - * @return object - */ - public function subject() { - return $this->_subject; - } - -/** - * Stops the event from being used anymore - * - * @return bool - */ - public function stopPropagation() { - return $this->_stopped = true; - } - -/** - * Check if the event is stopped - * - * @return bool True if the event is stopped - */ - public function isStopped() { - return $this->_stopped; - } + /** + * Check if the event is stopped + * + * @return bool True if the event is stopped + */ + public function isStopped() + { + return $this->_stopped; + } } diff --git a/lib/Cake/Event/CakeEventListener.php b/lib/Cake/Event/CakeEventListener.php index 86890656..864e2967 100755 --- a/lib/Cake/Event/CakeEventListener.php +++ b/lib/Cake/Event/CakeEventListener.php @@ -7,10 +7,10 @@ * For full copyright and license information, please see the LICENSE.txt * Redistributions of files must retain the above copyright notice. * - * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org) - * @link https://cakephp.org CakePHP(tm) Project - * @package Cake.Observer - * @since CakePHP(tm) v 2.1 + * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org) + * @link https://cakephp.org CakePHP(tm) Project + * @package Cake.Observer + * @since CakePHP(tm) v 2.1 * @license https://opensource.org/licenses/mit-license.php MIT License */ @@ -20,27 +20,28 @@ * * @package Cake.Event */ -interface CakeEventListener { +interface CakeEventListener +{ -/** - * Returns a list of events this object is implementing. When the class is registered - * in an event manager, each individual method will be associated with the respective event. - * - * ## Example: - * - * ``` - * public function implementedEvents() { - * return array( - * 'Order.complete' => 'sendEmail', - * 'Article.afterBuy' => 'decrementInventory', - * 'User.onRegister' => array('callable' => 'logRegistration', 'priority' => 20, 'passParams' => true) - * ); - * } - * ``` - * - * @return array associative array or event key names pointing to the function - * that should be called in the object when the respective event is fired - */ - public function implementedEvents(); + /** + * Returns a list of events this object is implementing. When the class is registered + * in an event manager, each individual method will be associated with the respective event. + * + * ## Example: + * + * ``` + * public function implementedEvents() { + * return array( + * 'Order.complete' => 'sendEmail', + * 'Article.afterBuy' => 'decrementInventory', + * 'User.onRegister' => array('callable' => 'logRegistration', 'priority' => 20, 'passParams' => true) + * ); + * } + * ``` + * + * @return array associative array or event key names pointing to the function + * that should be called in the object when the respective event is fired + */ + public function implementedEvents(); } diff --git a/lib/Cake/Event/CakeEventManager.php b/lib/Cake/Event/CakeEventManager.php index 448bc530..db56e2d1 100755 --- a/lib/Cake/Event/CakeEventManager.php +++ b/lib/Cake/Event/CakeEventManager.php @@ -7,10 +7,10 @@ * For full copyright and license information, please see the LICENSE.txt * Redistributions of files must retain the above copyright notice. * - * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org) - * @link https://cakephp.org CakePHP(tm) Project - * @package Cake.Event - * @since CakePHP(tm) v 2.1 + * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org) + * @link https://cakephp.org CakePHP(tm) Project + * @package Cake.Event + * @since CakePHP(tm) v 2.1 * @license https://opensource.org/licenses/mit-license.php MIT License */ @@ -25,7 +25,8 @@ * * @package Cake.Event */ -class CakeEventManager { +class CakeEventManager +{ /** * The default priority queue value for new, attached listeners @@ -46,7 +47,7 @@ class CakeEventManager { * * @var object */ - protected $_listeners = array(); + protected $_listeners = []; /** * Internal flag to distinguish a common manager from the singleton @@ -55,29 +56,6 @@ class CakeEventManager { */ protected $_isGlobal = false; - /** - * Returns the globally available instance of a CakeEventManager - * this is used for dispatching events attached from outside the scope - * other managers were created. Usually for creating hook systems or inter-class - * communication - * - * If called with the first parameter, it will be set as the globally available instance - * - * @param CakeEventManager $manager Optional event manager instance. - * @return CakeEventManager the global event manager - */ - public static function instance($manager = null) { - if ($manager instanceof CakeEventManager) { - static::$_generalManager = $manager; - } - if (empty(static::$_generalManager)) { - static::$_generalManager = new CakeEventManager(); - } - - static::$_generalManager->_isGlobal = true; - return static::$_generalManager; - } - /** * Adds a new listener to an event. Listeners * @@ -98,7 +76,8 @@ public static function instance($manager = null) { * @throws InvalidArgumentException When event key is missing or callable is not an * instance of CakeEventListener. */ - public function attach($callable, $eventKey = null, $options = array()) { + public function attach($callable, $eventKey = null, $options = []) + { if (!$eventKey && !($callable instanceof CakeEventListener)) { throw new InvalidArgumentException(__d('cake_dev', 'The eventKey variable is required')); } @@ -106,11 +85,11 @@ public function attach($callable, $eventKey = null, $options = array()) { $this->_attachSubscriber($callable); return; } - $options = $options + array('priority' => static::$defaultPriority, 'passParams' => false); - $this->_listeners[$eventKey][$options['priority']][] = array( + $options = $options + ['priority' => static::$defaultPriority, 'passParams' => false]; + $this->_listeners[$eventKey][$options['priority']][] = [ 'callable' => $callable, 'passParams' => $options['passParams'], - ); + ]; } /** @@ -120,13 +99,14 @@ public function attach($callable, $eventKey = null, $options = array()) { * @param CakeEventListener $subscriber Event listener. * @return void */ - protected function _attachSubscriber(CakeEventListener $subscriber) { + protected function _attachSubscriber(CakeEventListener $subscriber) + { foreach ((array)$subscriber->implementedEvents() as $eventKey => $function) { - $options = array(); + $options = []; $method = $function; if (is_array($function) && isset($function['callable'])) { list($method, $options) = $this->_extractCallable($function, $subscriber); - } elseif (is_array($function) && is_numeric(key($function))) { + } else if (is_array($function) && is_numeric(key($function))) { foreach ($function as $f) { list($method, $options) = $this->_extractCallable($f, $subscriber); $this->attach($method, $eventKey, $options); @@ -134,7 +114,7 @@ protected function _attachSubscriber(CakeEventListener $subscriber) { continue; } if (is_string($method)) { - $method = array($subscriber, $function); + $method = [$subscriber, $function]; } $this->attach($method, $eventKey, $options); } @@ -148,14 +128,15 @@ protected function _attachSubscriber(CakeEventListener $subscriber) { * @param CakeEventListener $object The handler object * @return callable */ - protected function _extractCallable($function, $object) { + protected function _extractCallable($function, $object) + { $method = $function['callable']; $options = $function; unset($options['callable']); if (is_string($method)) { - $method = array($object, $method); + $method = [$object, $method]; } - return array($method, $options); + return [$method, $options]; } /** @@ -165,7 +146,8 @@ protected function _extractCallable($function, $object) { * @param string $eventKey The event unique identifier name with which the callback has been associated * @return void */ - public function detach($callable, $eventKey = null) { + public function detach($callable, $eventKey = null) + { if ($callable instanceof CakeEventListener) { return $this->_detachSubscriber($callable, $eventKey); } @@ -195,25 +177,26 @@ public function detach($callable, $eventKey = null) { * @param string $eventKey optional event key name to unsubscribe the listener from * @return void */ - protected function _detachSubscriber(CakeEventListener $subscriber, $eventKey = null) { + protected function _detachSubscriber(CakeEventListener $subscriber, $eventKey = null) + { $events = (array)$subscriber->implementedEvents(); if (!empty($eventKey) && empty($events[$eventKey])) { return; - } elseif (!empty($eventKey)) { - $events = array($eventKey => $events[$eventKey]); + } else if (!empty($eventKey)) { + $events = [$eventKey => $events[$eventKey]]; } foreach ($events as $key => $function) { if (is_array($function)) { if (is_numeric(key($function))) { foreach ($function as $handler) { $handler = isset($handler['callable']) ? $handler['callable'] : $handler; - $this->detach(array($subscriber, $handler), $key); + $this->detach([$subscriber, $handler], $key); } continue; } $function = $function['callable']; } - $this->detach(array($subscriber, $function), $key); + $this->detach([$subscriber, $function], $key); } } @@ -224,7 +207,8 @@ protected function _detachSubscriber(CakeEventListener $subscriber, $eventKey = * @return CakeEvent * @triggers $event */ - public function dispatch($event) { + public function dispatch($event) + { if (is_string($event)) { $event = new CakeEvent($event); } @@ -259,21 +243,22 @@ public function dispatch($event) { * @param string $eventKey Event key. * @return array */ - public function listeners($eventKey) { - $localListeners = array(); - $priorities = array(); + public function listeners($eventKey) + { + $localListeners = []; + $priorities = []; if (!$this->_isGlobal) { $localListeners = $this->prioritisedListeners($eventKey); - $localListeners = empty($localListeners) ? array() : $localListeners; + $localListeners = empty($localListeners) ? [] : $localListeners; } $globalListeners = static::instance()->prioritisedListeners($eventKey); - $globalListeners = empty($globalListeners) ? array() : $globalListeners; + $globalListeners = empty($globalListeners) ? [] : $globalListeners; $priorities = array_merge(array_keys($globalListeners), array_keys($localListeners)); $priorities = array_unique($priorities); asort($priorities); - $result = array(); + $result = []; foreach ($priorities as $priority) { if (isset($globalListeners[$priority])) { $result = array_merge($result, $globalListeners[$priority]); @@ -291,10 +276,35 @@ public function listeners($eventKey) { * @param string $eventKey Event key. * @return array */ - public function prioritisedListeners($eventKey) { + public function prioritisedListeners($eventKey) + { if (empty($this->_listeners[$eventKey])) { - return array(); + return []; } return $this->_listeners[$eventKey]; } + + /** + * Returns the globally available instance of a CakeEventManager + * this is used for dispatching events attached from outside the scope + * other managers were created. Usually for creating hook systems or inter-class + * communication + * + * If called with the first parameter, it will be set as the globally available instance + * + * @param CakeEventManager $manager Optional event manager instance. + * @return CakeEventManager the global event manager + */ + public static function instance($manager = null) + { + if ($manager instanceof CakeEventManager) { + static::$_generalManager = $manager; + } + if (empty(static::$_generalManager)) { + static::$_generalManager = new CakeEventManager(); + } + + static::$_generalManager->_isGlobal = true; + return static::$_generalManager; + } } \ No newline at end of file diff --git a/lib/Cake/I18n/I18n.php b/lib/Cake/I18n/I18n.php index 814c2cfe..12a9433f 100755 --- a/lib/Cake/I18n/I18n.php +++ b/lib/Cake/I18n/I18n.php @@ -26,721 +26,721 @@ * * @package Cake.I18n */ -class I18n { - -/** - * Instance of the L10n class for localization - * - * @var L10n - */ - public $l10n = null; - -/** - * Default domain of translation - * - * @var string - */ - public static $defaultDomain = 'default'; - -/** - * Current domain of translation - * - * @var string - */ - public $domain = null; - -/** - * Current category of translation - * - * @var string - */ - public $category = 'LC_MESSAGES'; - -/** - * Current language used for translations - * - * @var string - */ - protected $_lang = null; - -/** - * Translation strings for a specific domain read from the .mo or .po files - * - * @var array - */ - protected $_domains = array(); - -/** - * Set to true when I18N::_bindTextDomain() is called for the first time. - * If a translation file is found it is set to false again - * - * @var bool - */ - protected $_noLocale = false; - -/** - * Translation categories - * - * @var array - */ - protected $_categories = array( - 'LC_ALL', 'LC_COLLATE', 'LC_CTYPE', 'LC_MONETARY', 'LC_NUMERIC', 'LC_TIME', 'LC_MESSAGES' - ); - -/** - * Constants for the translation categories. - * - * The constants may be used in translation fetching - * instead of hardcoded integers. - * Example: - * ``` - * I18n::translate('CakePHP is awesome.', null, null, I18n::LC_MESSAGES) - * ``` - * - * To keep the code more readable, I18n constants are preferred over - * hardcoded integers. - */ -/** - * Constant for LC_ALL. - * - * @var int - */ - const LC_ALL = 0; - -/** - * Constant for LC_COLLATE. - * - * @var int - */ - const LC_COLLATE = 1; - -/** - * Constant for LC_CTYPE. - * - * @var int - */ - const LC_CTYPE = 2; - -/** - * Constant for LC_MONETARY. - * - * @var int - */ - const LC_MONETARY = 3; - -/** - * Constant for LC_NUMERIC. - * - * @var int - */ - const LC_NUMERIC = 4; - -/** - * Constant for LC_TIME. - * - * @var int - */ - const LC_TIME = 5; - -/** - * Constant for LC_MESSAGES. - * - * @var int - */ - const LC_MESSAGES = 6; - -/** - * Escape string - * - * @var string - */ - protected $_escape = null; - -/** - * Constructor, use I18n::getInstance() to get the i18n translation object. - */ - public function __construct() { - $this->l10n = new L10n(); - } - -/** - * Return a static instance of the I18n class - * - * @return I18n - */ - public static function getInstance() { - static $instance = array(); - if (!$instance) { - $instance[0] = new I18n(); - } - return $instance[0]; - } - -/** - * Used by the translation functions in basics.php - * Returns a translated string based on current language and translation files stored in locale folder - * - * @param string $singular String to translate - * @param string $plural Plural string (if any) - * @param string $domain Domain The domain of the translation. Domains are often used by plugin translations. - * If null, the default domain will be used. - * @param string $category Category The integer value of the category to use. - * @param int $count Count Count is used with $plural to choose the correct plural form. - * @param string $language Language to translate string to. - * If null it checks for language in session followed by Config.language configuration variable. - * @param string $context Context The context of the translation, e.g a verb or a noun. - * @return string translated string. - * @throws CakeException When '' is provided as a domain. - */ - public static function translate($singular, $plural = null, $domain = null, $category = self::LC_MESSAGES, - $count = null, $language = null, $context = null - ) { - $_this = I18n::getInstance(); - - if (strpos($singular, "\r\n") !== false) { - $singular = str_replace("\r\n", "\n", $singular); - } - if ($plural !== null && strpos($plural, "\r\n") !== false) { - $plural = str_replace("\r\n", "\n", $plural); - } - - if (is_numeric($category)) { - $_this->category = $_this->_categories[$category]; - } - - if (empty($language)) { - if (CakeSession::started()) { - $language = CakeSession::read('Config.language'); - } - if (empty($language)) { - $language = Configure::read('Config.language'); - } - } - - if (($_this->_lang && $_this->_lang !== $language) || !$_this->_lang) { - $lang = $_this->l10n->get($language); - $_this->_lang = $lang; - } - - if ($domain === null) { - $domain = static::$defaultDomain; - } - if ($domain === '') { - throw new CakeException(__d('cake_dev', 'You cannot use "" as a domain.')); - } - - $_this->domain = $domain . '_' . $_this->l10n->lang; - - if (!isset($_this->_domains[$domain][$_this->_lang])) { - $_this->_domains[$domain][$_this->_lang] = Cache::read($_this->domain, '_cake_core_'); - } - - if (!isset($_this->_domains[$domain][$_this->_lang][$_this->category])) { - $_this->_bindTextDomain($domain); - Cache::write($_this->domain, $_this->_domains[$domain][$_this->_lang], '_cake_core_'); - } - - if ($_this->category === 'LC_TIME') { - return $_this->_translateTime($singular, $domain); - } - - if (!isset($count)) { - $plurals = 0; - } elseif (!empty($_this->_domains[$domain][$_this->_lang][$_this->category]["%plural-c"]) && $_this->_noLocale === false) { - $header = $_this->_domains[$domain][$_this->_lang][$_this->category]["%plural-c"]; - $plurals = $_this->_pluralGuess($header, $count); - } else { - if ($count != 1) { - $plurals = 1; - } else { - $plurals = 0; - } - } - - if (!empty($_this->_domains[$domain][$_this->_lang][$_this->category][$singular][$context])) { - if (($trans = $_this->_domains[$domain][$_this->_lang][$_this->category][$singular][$context]) || - ($plurals) && ($trans = $_this->_domains[$domain][$_this->_lang][$_this->category][$plural][$context]) - ) { - if (is_array($trans)) { - if (isset($trans[$plurals])) { - $trans = $trans[$plurals]; - } else { - trigger_error( - __d('cake_dev', - 'Missing plural form translation for "%s" in "%s" domain, "%s" locale. ' . - ' Check your po file for correct plurals and valid Plural-Forms header.', - $singular, - $domain, - $_this->_lang - ), - E_USER_WARNING - ); - $trans = $trans[0]; - } - } - if (strlen($trans)) { - return $trans; - } - } - } - - if (!empty($plurals)) { - return $plural; - } - return $singular; - } - -/** - * Clears the domains internal data array. Useful for testing i18n. - * - * @return void - */ - public static function clear() { - $self = I18n::getInstance(); - $self->_domains = array(); - } - -/** - * Get the loaded domains cache. - * - * @return array - */ - public static function domains() { - $self = I18n::getInstance(); - return $self->_domains; - } - -/** - * Attempts to find the plural form of a string. - * - * @param string $header Type - * @param int $n Number - * @return int plural match - * @link http://localization-guide.readthedocs.org/en/latest/l10n/pluralforms.html - * @link https://developer.mozilla.org/en-US/docs/Mozilla/Localization/Localization_and_Plurals#List_of_Plural_Rules - */ - protected function _pluralGuess($header, $n) { - if (!is_string($header) || $header === "nplurals=1;plural=0;" || !isset($header[0])) { - return 0; - } - - if ($header === "nplurals=2;plural=n!=1;") { - return $n != 1 ? 1 : 0; - } elseif ($header === "nplurals=2;plural=n>1;") { - return $n > 1 ? 1 : 0; - } - - if (strpos($header, "plurals=3")) { - if (strpos($header, "100!=11")) { - if (strpos($header, "10<=4")) { - return $n % 10 == 1 && $n % 100 != 11 ? 0 : ($n % 10 >= 2 && $n % 10 <= 4 && ($n % 100 < 10 || $n % 100 >= 20) ? 1 : 2); - } elseif (strpos($header, "100<10")) { - return $n % 10 == 1 && $n % 100 != 11 ? 0 : ($n % 10 >= 2 && ($n % 100 < 10 || $n % 100 >= 20) ? 1 : 2); - } - return $n % 10 == 1 && $n % 100 != 11 ? 0 : ($n != 0 ? 1 : 2); - } elseif (strpos($header, "n==2")) { - return $n == 1 ? 0 : ($n == 2 ? 1 : 2); - } elseif (strpos($header, "n==0")) { - return $n == 1 ? 0 : ($n == 0 || ($n % 100 > 0 && $n % 100 < 20) ? 1 : 2); - } elseif (strpos($header, "n>=2")) { - return $n == 1 ? 0 : ($n >= 2 && $n <= 4 ? 1 : 2); - } elseif (strpos($header, "10>=2")) { - return $n == 1 ? 0 : ($n % 10 >= 2 && $n % 10 <= 4 && ($n % 100 < 10 || $n % 100 >= 20) ? 1 : 2); - } - return $n % 10 == 1 ? 0 : ($n % 10 == 2 ? 1 : 2); - } elseif (strpos($header, "plurals=4")) { - if (strpos($header, "100==2")) { - return $n % 100 == 1 ? 0 : ($n % 100 == 2 ? 1 : ($n % 100 == 3 || $n % 100 == 4 ? 2 : 3)); - } elseif (strpos($header, "n>=3")) { - return $n == 1 ? 0 : ($n == 2 ? 1 : ($n == 0 || ($n >= 3 && $n <= 10) ? 2 : 3)); - } elseif (strpos($header, "100>=1")) { - return $n == 1 ? 0 : ($n == 0 || ($n % 100 >= 1 && $n % 100 <= 10) ? 1 : ($n % 100 >= 11 && $n % 100 <= 20 ? 2 : 3)); - } - } elseif (strpos($header, "plurals=5")) { - return $n == 1 ? 0 : ($n == 2 ? 1 : ($n >= 3 && $n <= 6 ? 2 : ($n >= 7 && $n <= 10 ? 3 : 4))); - } elseif (strpos($header, "plurals=6")) { - return $n == 0 ? 0 : - ($n == 1 ? 1 : - ($n == 2 ? 2 : - ($n % 100 >= 3 && $n % 100 <= 10 ? 3 : - ($n % 100 >= 11 ? 4 : 5)))); - } - - return 0; - } - -/** - * Binds the given domain to a file in the specified directory. - * - * @param string $domain Domain to bind - * @return string Domain binded - */ - protected function _bindTextDomain($domain) { - $this->_noLocale = true; - $core = true; - $merge = array(); - $searchPaths = App::path('locales'); - $plugins = CakePlugin::loaded(); - - if (!empty($plugins)) { - foreach ($plugins as $plugin) { - $pluginDomain = Inflector::underscore($plugin); - if ($pluginDomain === $domain) { - $searchPaths[] = CakePlugin::path($plugin) . 'Locale' . DS; - if (!Configure::read('I18n.preferApp')) { - $searchPaths = array_reverse($searchPaths); - } - break; - } - } - } - - foreach ($searchPaths as $directory) { - foreach ($this->l10n->languagePath as $lang) { - $localeDef = $directory . $lang . DS . $this->category; - if (is_file($localeDef)) { - $definitions = static::loadLocaleDefinition($localeDef); - if ($definitions !== false) { - $this->_domains[$domain][$this->_lang][$this->category] = $definitions; - $this->_noLocale = false; - return $domain; - } - } - - if ($core) { - $app = $directory . $lang . DS . $this->category . DS . 'core'; - $translations = false; - - if (is_file($app . '.mo')) { - $translations = static::loadMo($app . '.mo'); - } - if ($translations === false && is_file($app . '.po')) { - $translations = static::loadPo($app . '.po'); - } - - if ($translations !== false) { - $this->_domains[$domain][$this->_lang][$this->category] = $translations; - $merge[$domain][$this->_lang][$this->category] = $this->_domains[$domain][$this->_lang][$this->category]; - $this->_noLocale = false; - $core = null; - } - } - - $file = $directory . $lang . DS . $this->category . DS . $domain; - $translations = false; - - if (is_file($file . '.mo')) { - $translations = static::loadMo($file . '.mo'); - } - if ($translations === false && is_file($file . '.po')) { - $translations = static::loadPo($file . '.po'); - } - - if ($translations !== false) { - $this->_domains[$domain][$this->_lang][$this->category] = $translations; - $this->_noLocale = false; - break 2; - } - } - } - - if (empty($this->_domains[$domain][$this->_lang][$this->category])) { - $this->_domains[$domain][$this->_lang][$this->category] = array(); - return $domain; - } - - if (isset($this->_domains[$domain][$this->_lang][$this->category][""])) { - $head = $this->_domains[$domain][$this->_lang][$this->category][""]; - - foreach (explode("\n", $head) as $line) { - $header = strtok($line, ':'); - $line = trim(strtok("\n")); - $this->_domains[$domain][$this->_lang][$this->category]["%po-header"][strtolower($header)] = $line; - } - - if (isset($this->_domains[$domain][$this->_lang][$this->category]["%po-header"]["plural-forms"])) { - $switch = preg_replace("/(?:[() {}\\[\\]^\\s*\\]]+)/", "", $this->_domains[$domain][$this->_lang][$this->category]["%po-header"]["plural-forms"]); - $this->_domains[$domain][$this->_lang][$this->category]["%plural-c"] = $switch; - unset($this->_domains[$domain][$this->_lang][$this->category]["%po-header"]); - } - $this->_domains = Hash::mergeDiff($this->_domains, $merge); - - if (isset($this->_domains[$domain][$this->_lang][$this->category][null])) { - unset($this->_domains[$domain][$this->_lang][$this->category][null]); - } - } - - return $domain; - } - -/** - * Loads the binary .mo file and returns array of translations - * - * @param string $filename Binary .mo file to load - * @return mixed Array of translations on success or false on failure - * @link https://www.gnu.org/software/gettext/manual/html_node/MO-Files.html - */ - public static function loadMo($filename) { - $translations = false; - - // @codingStandardsIgnoreStart - // Binary files extracted makes non-standard local variables - if ($data = file_get_contents($filename)) { - $translations = array(); - $header = substr($data, 0, 20); - $header = unpack('L1magic/L1version/L1count/L1o_msg/L1o_trn', $header); - extract($header); - - if ((dechex($magic) === '950412de' || dechex($magic) === 'ffffffff950412de') && !$version) { - for ($n = 0; $n < $count; $n++) { - $r = unpack("L1len/L1offs", substr($data, $o_msg + $n * 8, 8)); - $msgid = substr($data, $r["offs"], $r["len"]); - unset($msgid_plural); - $context = null; - - if (strpos($msgid, "\x04") !== false) { - list($context, $msgid) = explode("\x04", $msgid); - } - if (strpos($msgid, "\000")) { - list($msgid, $msgid_plural) = explode("\000", $msgid); - } - $r = unpack("L1len/L1offs", substr($data, $o_trn + $n * 8, 8)); - $msgstr = substr($data, $r["offs"], $r["len"]); - - if (strpos($msgstr, "\000")) { - $msgstr = explode("\000", $msgstr); - } - - if ($msgid != '') { - $translations[$msgid][$context] = $msgstr; - } else { - $translations[$msgid] = $msgstr; - } - - if (isset($msgid_plural)) { - $translations[$msgid_plural] =& $translations[$msgid]; - } - } - } - } - // @codingStandardsIgnoreEnd - - return $translations; - } - -/** - * Loads the text .po file and returns array of translations - * - * @param string $filename Text .po file to load - * @return mixed Array of translations on success or false on failure - */ - public static function loadPo($filename) { - if (!$file = fopen($filename, 'r')) { - return false; - } - - $type = 0; - $translations = array(); - $translationKey = ''; - $translationContext = null; - $plural = 0; - $header = ''; - - do { - $line = trim(fgets($file)); - if ($line === '' || $line[0] === '#') { - $translationContext = null; - - continue; - } - if (preg_match("/msgid[[:space:]]+\"(.+)\"$/i", $line, $regs)) { - $type = 1; - $translationKey = stripcslashes($regs[1]); - } elseif (preg_match("/msgid[[:space:]]+\"\"$/i", $line, $regs)) { - $type = 2; - $translationKey = ''; - } elseif (preg_match("/msgctxt[[:space:]]+\"(.+)\"$/i", $line, $regs)) { - $translationContext = $regs[1]; - } elseif (preg_match("/^\"(.*)\"$/i", $line, $regs) && ($type == 1 || $type == 2 || $type == 3)) { - $type = 3; - $translationKey .= stripcslashes($regs[1]); - } elseif (preg_match("/msgstr[[:space:]]+\"(.+)\"$/i", $line, $regs) && ($type == 1 || $type == 3) && $translationKey) { - $translations[$translationKey][$translationContext] = stripcslashes($regs[1]); - $type = 4; - } elseif (preg_match("/msgstr[[:space:]]+\"\"$/i", $line, $regs) && ($type == 1 || $type == 3) && $translationKey) { - $type = 4; - $translations[$translationKey][$translationContext] = ''; - } elseif (preg_match("/^\"(.*)\"$/i", $line, $regs) && $type == 4 && $translationKey) { - $translations[$translationKey][$translationContext] .= stripcslashes($regs[1]); - } elseif (preg_match("/msgid_plural[[:space:]]+\".*\"$/i", $line, $regs)) { - $type = 6; - } elseif (preg_match("/^\"(.*)\"$/i", $line, $regs) && $type == 6 && $translationKey) { - $type = 6; - } elseif (preg_match("/msgstr\[(\d+)\][[:space:]]+\"(.+)\"$/i", $line, $regs) && ($type == 6 || $type == 7) && $translationKey) { - $plural = $regs[1]; - $translations[$translationKey][$translationContext][$plural] = stripcslashes($regs[2]); - $type = 7; - } elseif (preg_match("/msgstr\[(\d+)\][[:space:]]+\"\"$/i", $line, $regs) && ($type == 6 || $type == 7) && $translationKey) { - $plural = $regs[1]; - $translations[$translationKey][$translationContext][$plural] = ''; - $type = 7; - } elseif (preg_match("/^\"(.*)\"$/i", $line, $regs) && $type == 7 && $translationKey) { - $translations[$translationKey][$translationContext][$plural] .= stripcslashes($regs[1]); - } elseif (preg_match("/msgstr[[:space:]]+\"(.+)\"$/i", $line, $regs) && $type == 2 && !$translationKey) { - $header .= stripcslashes($regs[1]); - $type = 5; - } elseif (preg_match("/msgstr[[:space:]]+\"\"$/i", $line, $regs) && !$translationKey) { - $header = ''; - $type = 5; - } elseif (preg_match("/^\"(.*)\"$/i", $line, $regs) && $type == 5) { - $header .= stripcslashes($regs[1]); - } else { - unset($translations[$translationKey][$translationContext]); - $type = 0; - $translationKey = ''; - $translationContext = null; - $plural = 0; - } - } while (!feof($file)); - fclose($file); - - $merge[''] = $header; - return array_merge($merge, $translations); - } - -/** - * Parses a locale definition file following the POSIX standard - * - * @param string $filename Locale definition filename - * @return mixed Array of definitions on success or false on failure - */ - public static function loadLocaleDefinition($filename) { - if (!$file = fopen($filename, 'r')) { - return false; - } - - $definitions = array(); - $comment = '#'; - $escape = '\\'; - $currentToken = false; - $value = ''; - $_this = I18n::getInstance(); - while ($line = fgets($file)) { - $line = trim($line); - if (empty($line) || $line[0] === $comment) { - continue; - } - $parts = preg_split("/[[:space:]]+/", $line); - if ($parts[0] === 'comment_char') { - $comment = $parts[1]; - continue; - } - if ($parts[0] === 'escape_char') { - $escape = $parts[1]; - continue; - } - $count = count($parts); - if ($count === 2) { - $currentToken = $parts[0]; - $value = $parts[1]; - } elseif ($count === 1) { - $value = is_array($value) ? $parts[0] : $value . $parts[0]; - } else { - continue; - } - - $len = strlen($value) - 1; - if ($value[$len] === $escape) { - $value = substr($value, 0, $len); - continue; - } - - $mustEscape = array($escape . ',', $escape . ';', $escape . '<', $escape . '>', $escape . $escape); - $replacements = array_map('crc32', $mustEscape); - $value = str_replace($mustEscape, $replacements, $value); - $value = explode(';', $value); - $_this->_escape = $escape; - foreach ($value as $i => $val) { - $val = trim($val, '"'); - $val = preg_replace_callback('/(?:<)?(.[^>]*)(?:>)?/', array(&$_this, '_parseLiteralValue'), $val); - $val = str_replace($replacements, $mustEscape, $val); - $value[$i] = $val; - } - if (count($value) === 1) { - $definitions[$currentToken] = array_pop($value); - } else { - $definitions[$currentToken] = $value; - } - } - - return $definitions; - } - -/** - * Puts the parameters in raw translated strings - * - * @param string $translated The raw translated string - * @param array $args The arguments to put in the translation - * @return string Translated string with arguments - */ - public static function insertArgs($translated, array $args) { - $len = count($args); - if ($len === 0 || ($len === 1 && $args[0] === null)) { - return $translated; - } - - if (is_array($args[0])) { - $args = $args[0]; - } - - $translated = preg_replace('/(?_escape . 'x') { - $delimiter = $this->_escape . 'x'; - return implode('', array_map('chr', array_map('hexdec', array_filter(explode($delimiter, $string))))); - } - if (substr($string, 0, 2) === $this->_escape . 'd') { - $delimiter = $this->_escape . 'd'; - return implode('', array_map('chr', array_filter(explode($delimiter, $string)))); - } - if ($string[0] === $this->_escape && isset($string[1]) && is_numeric($string[1])) { - $delimiter = $this->_escape; - return implode('', array_map('chr', array_filter(explode($delimiter, $string)))); - } - if (substr($string, 0, 3) === 'U00') { - $delimiter = 'U00'; - return implode('', array_map('chr', array_map('hexdec', array_filter(explode($delimiter, $string))))); - } - if (preg_match('/U([0-9a-fA-F]{4})/', $string, $match)) { - return Multibyte::ascii(array(hexdec($match[1]))); - } - return $string; - } - -/** - * Returns a Time format definition from corresponding domain - * - * @param string $format Format to be translated - * @param string $domain Domain where format is stored - * @return mixed translated format string if only value or array of translated strings for corresponding format. - */ - protected function _translateTime($format, $domain) { - if (!empty($this->_domains[$domain][$this->_lang]['LC_TIME'][$format])) { - if (($trans = $this->_domains[$domain][$this->_lang][$this->category][$format])) { - return $trans; - } - } - return $format; - } +class I18n +{ + + /** + * Constant for LC_ALL. + * + * @var int + */ + const LC_ALL = 0; + /** + * Constant for LC_COLLATE. + * + * @var int + */ + const LC_COLLATE = 1; + /** + * Constant for LC_CTYPE. + * + * @var int + */ + const LC_CTYPE = 2; + /** + * Constant for LC_MONETARY. + * + * @var int + */ + const LC_MONETARY = 3; + /** + * Constant for LC_NUMERIC. + * + * @var int + */ + const LC_NUMERIC = 4; + /** + * Constant for LC_TIME. + * + * @var int + */ + const LC_TIME = 5; + /** + * Constant for LC_MESSAGES. + * + * @var int + */ + const LC_MESSAGES = 6; + /** + * Default domain of translation + * + * @var string + */ + public static $defaultDomain = 'default'; + + /** + * Constants for the translation categories. + * + * The constants may be used in translation fetching + * instead of hardcoded integers. + * Example: + * ``` + * I18n::translate('CakePHP is awesome.', null, null, I18n::LC_MESSAGES) + * ``` + * + * To keep the code more readable, I18n constants are preferred over + * hardcoded integers. + */ + /** + * Instance of the L10n class for localization + * + * @var L10n + */ + public $l10n = null; + /** + * Current domain of translation + * + * @var string + */ + public $domain = null; + /** + * Current category of translation + * + * @var string + */ + public $category = 'LC_MESSAGES'; + /** + * Current language used for translations + * + * @var string + */ + protected $_lang = null; + /** + * Translation strings for a specific domain read from the .mo or .po files + * + * @var array + */ + protected $_domains = []; + /** + * Set to true when I18N::_bindTextDomain() is called for the first time. + * If a translation file is found it is set to false again + * + * @var bool + */ + protected $_noLocale = false; + /** + * Translation categories + * + * @var array + */ + protected $_categories = [ + 'LC_ALL', 'LC_COLLATE', 'LC_CTYPE', 'LC_MONETARY', 'LC_NUMERIC', 'LC_TIME', 'LC_MESSAGES' + ]; + /** + * Escape string + * + * @var string + */ + protected $_escape = null; + + /** + * Constructor, use I18n::getInstance() to get the i18n translation object. + */ + public function __construct() + { + $this->l10n = new L10n(); + } + + /** + * Used by the translation functions in basics.php + * Returns a translated string based on current language and translation files stored in locale folder + * + * @param string $singular String to translate + * @param string $plural Plural string (if any) + * @param string $domain Domain The domain of the translation. Domains are often used by plugin translations. + * If null, the default domain will be used. + * @param string $category Category The integer value of the category to use. + * @param int $count Count Count is used with $plural to choose the correct plural form. + * @param string $language Language to translate string to. + * If null it checks for language in session followed by Config.language configuration variable. + * @param string $context Context The context of the translation, e.g a verb or a noun. + * @return string translated string. + * @throws CakeException When '' is provided as a domain. + */ + public static function translate($singular, $plural = null, $domain = null, $category = self::LC_MESSAGES, + $count = null, $language = null, $context = null + ) + { + $_this = I18n::getInstance(); + + if (strpos($singular, "\r\n") !== false) { + $singular = str_replace("\r\n", "\n", $singular); + } + if ($plural !== null && strpos($plural, "\r\n") !== false) { + $plural = str_replace("\r\n", "\n", $plural); + } + + if (is_numeric($category)) { + $_this->category = $_this->_categories[$category]; + } + + if (empty($language)) { + if (CakeSession::started()) { + $language = CakeSession::read('Config.language'); + } + if (empty($language)) { + $language = Configure::read('Config.language'); + } + } + + if (($_this->_lang && $_this->_lang !== $language) || !$_this->_lang) { + $lang = $_this->l10n->get($language); + $_this->_lang = $lang; + } + + if ($domain === null) { + $domain = static::$defaultDomain; + } + if ($domain === '') { + throw new CakeException(__d('cake_dev', 'You cannot use "" as a domain.')); + } + + $_this->domain = $domain . '_' . $_this->l10n->lang; + + if (!isset($_this->_domains[$domain][$_this->_lang])) { + $_this->_domains[$domain][$_this->_lang] = Cache::read($_this->domain, '_cake_core_'); + } + + if (!isset($_this->_domains[$domain][$_this->_lang][$_this->category])) { + $_this->_bindTextDomain($domain); + Cache::write($_this->domain, $_this->_domains[$domain][$_this->_lang], '_cake_core_'); + } + + if ($_this->category === 'LC_TIME') { + return $_this->_translateTime($singular, $domain); + } + + if (!isset($count)) { + $plurals = 0; + } else if (!empty($_this->_domains[$domain][$_this->_lang][$_this->category]["%plural-c"]) && $_this->_noLocale === false) { + $header = $_this->_domains[$domain][$_this->_lang][$_this->category]["%plural-c"]; + $plurals = $_this->_pluralGuess($header, $count); + } else { + if ($count != 1) { + $plurals = 1; + } else { + $plurals = 0; + } + } + + if (!empty($_this->_domains[$domain][$_this->_lang][$_this->category][$singular][$context])) { + if (($trans = $_this->_domains[$domain][$_this->_lang][$_this->category][$singular][$context]) || + ($plurals) && ($trans = $_this->_domains[$domain][$_this->_lang][$_this->category][$plural][$context]) + ) { + if (is_array($trans)) { + if (isset($trans[$plurals])) { + $trans = $trans[$plurals]; + } else { + trigger_error( + __d('cake_dev', + 'Missing plural form translation for "%s" in "%s" domain, "%s" locale. ' . + ' Check your po file for correct plurals and valid Plural-Forms header.', + $singular, + $domain, + $_this->_lang + ), + E_USER_WARNING + ); + $trans = $trans[0]; + } + } + if (strlen($trans)) { + return $trans; + } + } + } + + if (!empty($plurals)) { + return $plural; + } + return $singular; + } + + /** + * Return a static instance of the I18n class + * + * @return I18n + */ + public static function getInstance() + { + static $instance = []; + if (!$instance) { + $instance[0] = new I18n(); + } + return $instance[0]; + } + + /** + * Binds the given domain to a file in the specified directory. + * + * @param string $domain Domain to bind + * @return string Domain binded + */ + protected function _bindTextDomain($domain) + { + $this->_noLocale = true; + $core = true; + $merge = []; + $searchPaths = App::path('locales'); + $plugins = CakePlugin::loaded(); + + if (!empty($plugins)) { + foreach ($plugins as $plugin) { + $pluginDomain = Inflector::underscore($plugin); + if ($pluginDomain === $domain) { + $searchPaths[] = CakePlugin::path($plugin) . 'Locale' . DS; + if (!Configure::read('I18n.preferApp')) { + $searchPaths = array_reverse($searchPaths); + } + break; + } + } + } + + foreach ($searchPaths as $directory) { + foreach ($this->l10n->languagePath as $lang) { + $localeDef = $directory . $lang . DS . $this->category; + if (is_file($localeDef)) { + $definitions = static::loadLocaleDefinition($localeDef); + if ($definitions !== false) { + $this->_domains[$domain][$this->_lang][$this->category] = $definitions; + $this->_noLocale = false; + return $domain; + } + } + + if ($core) { + $app = $directory . $lang . DS . $this->category . DS . 'core'; + $translations = false; + + if (is_file($app . '.mo')) { + $translations = static::loadMo($app . '.mo'); + } + if ($translations === false && is_file($app . '.po')) { + $translations = static::loadPo($app . '.po'); + } + + if ($translations !== false) { + $this->_domains[$domain][$this->_lang][$this->category] = $translations; + $merge[$domain][$this->_lang][$this->category] = $this->_domains[$domain][$this->_lang][$this->category]; + $this->_noLocale = false; + $core = null; + } + } + + $file = $directory . $lang . DS . $this->category . DS . $domain; + $translations = false; + + if (is_file($file . '.mo')) { + $translations = static::loadMo($file . '.mo'); + } + if ($translations === false && is_file($file . '.po')) { + $translations = static::loadPo($file . '.po'); + } + + if ($translations !== false) { + $this->_domains[$domain][$this->_lang][$this->category] = $translations; + $this->_noLocale = false; + break 2; + } + } + } + + if (empty($this->_domains[$domain][$this->_lang][$this->category])) { + $this->_domains[$domain][$this->_lang][$this->category] = []; + return $domain; + } + + if (isset($this->_domains[$domain][$this->_lang][$this->category][""])) { + $head = $this->_domains[$domain][$this->_lang][$this->category][""]; + + foreach (explode("\n", $head) as $line) { + $header = strtok($line, ':'); + $line = trim(strtok("\n")); + $this->_domains[$domain][$this->_lang][$this->category]["%po-header"][strtolower($header)] = $line; + } + + if (isset($this->_domains[$domain][$this->_lang][$this->category]["%po-header"]["plural-forms"])) { + $switch = preg_replace("/(?:[() {}\\[\\]^\\s*\\]]+)/", "", $this->_domains[$domain][$this->_lang][$this->category]["%po-header"]["plural-forms"]); + $this->_domains[$domain][$this->_lang][$this->category]["%plural-c"] = $switch; + unset($this->_domains[$domain][$this->_lang][$this->category]["%po-header"]); + } + $this->_domains = Hash::mergeDiff($this->_domains, $merge); + + if (isset($this->_domains[$domain][$this->_lang][$this->category][null])) { + unset($this->_domains[$domain][$this->_lang][$this->category][null]); + } + } + + return $domain; + } + + /** + * Parses a locale definition file following the POSIX standard + * + * @param string $filename Locale definition filename + * @return mixed Array of definitions on success or false on failure + */ + public static function loadLocaleDefinition($filename) + { + if (!$file = fopen($filename, 'r')) { + return false; + } + + $definitions = []; + $comment = '#'; + $escape = '\\'; + $currentToken = false; + $value = ''; + $_this = I18n::getInstance(); + while ($line = fgets($file)) { + $line = trim($line); + if (empty($line) || $line[0] === $comment) { + continue; + } + $parts = preg_split("/[[:space:]]+/", $line); + if ($parts[0] === 'comment_char') { + $comment = $parts[1]; + continue; + } + if ($parts[0] === 'escape_char') { + $escape = $parts[1]; + continue; + } + $count = count($parts); + if ($count === 2) { + $currentToken = $parts[0]; + $value = $parts[1]; + } else if ($count === 1) { + $value = is_array($value) ? $parts[0] : $value . $parts[0]; + } else { + continue; + } + + $len = strlen($value) - 1; + if ($value[$len] === $escape) { + $value = substr($value, 0, $len); + continue; + } + + $mustEscape = [$escape . ',', $escape . ';', $escape . '<', $escape . '>', $escape . $escape]; + $replacements = array_map('crc32', $mustEscape); + $value = str_replace($mustEscape, $replacements, $value); + $value = explode(';', $value); + $_this->_escape = $escape; + foreach ($value as $i => $val) { + $val = trim($val, '"'); + $val = preg_replace_callback('/(?:<)?(.[^>]*)(?:>)?/', [&$_this, '_parseLiteralValue'], $val); + $val = str_replace($replacements, $mustEscape, $val); + $value[$i] = $val; + } + if (count($value) === 1) { + $definitions[$currentToken] = array_pop($value); + } else { + $definitions[$currentToken] = $value; + } + } + + return $definitions; + } + + /** + * Loads the binary .mo file and returns array of translations + * + * @param string $filename Binary .mo file to load + * @return mixed Array of translations on success or false on failure + * @link https://www.gnu.org/software/gettext/manual/html_node/MO-Files.html + */ + public static function loadMo($filename) + { + $translations = false; + + // @codingStandardsIgnoreStart + // Binary files extracted makes non-standard local variables + if ($data = file_get_contents($filename)) { + $translations = []; + $header = substr($data, 0, 20); + $header = unpack('L1magic/L1version/L1count/L1o_msg/L1o_trn', $header); + extract($header); + + if ((dechex($magic) === '950412de' || dechex($magic) === 'ffffffff950412de') && !$version) { + for ($n = 0; $n < $count; $n++) { + $r = unpack("L1len/L1offs", substr($data, $o_msg + $n * 8, 8)); + $msgid = substr($data, $r["offs"], $r["len"]); + unset($msgid_plural); + $context = null; + + if (strpos($msgid, "\x04") !== false) { + list($context, $msgid) = explode("\x04", $msgid); + } + if (strpos($msgid, "\000")) { + list($msgid, $msgid_plural) = explode("\000", $msgid); + } + $r = unpack("L1len/L1offs", substr($data, $o_trn + $n * 8, 8)); + $msgstr = substr($data, $r["offs"], $r["len"]); + + if (strpos($msgstr, "\000")) { + $msgstr = explode("\000", $msgstr); + } + + if ($msgid != '') { + $translations[$msgid][$context] = $msgstr; + } else { + $translations[$msgid] = $msgstr; + } + + if (isset($msgid_plural)) { + $translations[$msgid_plural] =& $translations[$msgid]; + } + } + } + } + // @codingStandardsIgnoreEnd + + return $translations; + } + + /** + * Loads the text .po file and returns array of translations + * + * @param string $filename Text .po file to load + * @return mixed Array of translations on success or false on failure + */ + public static function loadPo($filename) + { + if (!$file = fopen($filename, 'r')) { + return false; + } + + $type = 0; + $translations = []; + $translationKey = ''; + $translationContext = null; + $plural = 0; + $header = ''; + + do { + $line = trim(fgets($file)); + if ($line === '' || $line[0] === '#') { + $translationContext = null; + + continue; + } + if (preg_match("/msgid[[:space:]]+\"(.+)\"$/i", $line, $regs)) { + $type = 1; + $translationKey = stripcslashes($regs[1]); + } else if (preg_match("/msgid[[:space:]]+\"\"$/i", $line, $regs)) { + $type = 2; + $translationKey = ''; + } else if (preg_match("/msgctxt[[:space:]]+\"(.+)\"$/i", $line, $regs)) { + $translationContext = $regs[1]; + } else if (preg_match("/^\"(.*)\"$/i", $line, $regs) && ($type == 1 || $type == 2 || $type == 3)) { + $type = 3; + $translationKey .= stripcslashes($regs[1]); + } else if (preg_match("/msgstr[[:space:]]+\"(.+)\"$/i", $line, $regs) && ($type == 1 || $type == 3) && $translationKey) { + $translations[$translationKey][$translationContext] = stripcslashes($regs[1]); + $type = 4; + } else if (preg_match("/msgstr[[:space:]]+\"\"$/i", $line, $regs) && ($type == 1 || $type == 3) && $translationKey) { + $type = 4; + $translations[$translationKey][$translationContext] = ''; + } else if (preg_match("/^\"(.*)\"$/i", $line, $regs) && $type == 4 && $translationKey) { + $translations[$translationKey][$translationContext] .= stripcslashes($regs[1]); + } else if (preg_match("/msgid_plural[[:space:]]+\".*\"$/i", $line, $regs)) { + $type = 6; + } else if (preg_match("/^\"(.*)\"$/i", $line, $regs) && $type == 6 && $translationKey) { + $type = 6; + } else if (preg_match("/msgstr\[(\d+)\][[:space:]]+\"(.+)\"$/i", $line, $regs) && ($type == 6 || $type == 7) && $translationKey) { + $plural = $regs[1]; + $translations[$translationKey][$translationContext][$plural] = stripcslashes($regs[2]); + $type = 7; + } else if (preg_match("/msgstr\[(\d+)\][[:space:]]+\"\"$/i", $line, $regs) && ($type == 6 || $type == 7) && $translationKey) { + $plural = $regs[1]; + $translations[$translationKey][$translationContext][$plural] = ''; + $type = 7; + } else if (preg_match("/^\"(.*)\"$/i", $line, $regs) && $type == 7 && $translationKey) { + $translations[$translationKey][$translationContext][$plural] .= stripcslashes($regs[1]); + } else if (preg_match("/msgstr[[:space:]]+\"(.+)\"$/i", $line, $regs) && $type == 2 && !$translationKey) { + $header .= stripcslashes($regs[1]); + $type = 5; + } else if (preg_match("/msgstr[[:space:]]+\"\"$/i", $line, $regs) && !$translationKey) { + $header = ''; + $type = 5; + } else if (preg_match("/^\"(.*)\"$/i", $line, $regs) && $type == 5) { + $header .= stripcslashes($regs[1]); + } else { + unset($translations[$translationKey][$translationContext]); + $type = 0; + $translationKey = ''; + $translationContext = null; + $plural = 0; + } + } while (!feof($file)); + fclose($file); + + $merge[''] = $header; + return array_merge($merge, $translations); + } + + /** + * Returns a Time format definition from corresponding domain + * + * @param string $format Format to be translated + * @param string $domain Domain where format is stored + * @return mixed translated format string if only value or array of translated strings for corresponding format. + */ + protected function _translateTime($format, $domain) + { + if (!empty($this->_domains[$domain][$this->_lang]['LC_TIME'][$format])) { + if (($trans = $this->_domains[$domain][$this->_lang][$this->category][$format])) { + return $trans; + } + } + return $format; + } + + /** + * Attempts to find the plural form of a string. + * + * @param string $header Type + * @param int $n Number + * @return int plural match + * @link http://localization-guide.readthedocs.org/en/latest/l10n/pluralforms.html + * @link https://developer.mozilla.org/en-US/docs/Mozilla/Localization/Localization_and_Plurals#List_of_Plural_Rules + */ + protected function _pluralGuess($header, $n) + { + if (!is_string($header) || $header === "nplurals=1;plural=0;" || !isset($header[0])) { + return 0; + } + + if ($header === "nplurals=2;plural=n!=1;") { + return $n != 1 ? 1 : 0; + } else if ($header === "nplurals=2;plural=n>1;") { + return $n > 1 ? 1 : 0; + } + + if (strpos($header, "plurals=3")) { + if (strpos($header, "100!=11")) { + if (strpos($header, "10<=4")) { + return $n % 10 == 1 && $n % 100 != 11 ? 0 : ($n % 10 >= 2 && $n % 10 <= 4 && ($n % 100 < 10 || $n % 100 >= 20) ? 1 : 2); + } else if (strpos($header, "100<10")) { + return $n % 10 == 1 && $n % 100 != 11 ? 0 : ($n % 10 >= 2 && ($n % 100 < 10 || $n % 100 >= 20) ? 1 : 2); + } + return $n % 10 == 1 && $n % 100 != 11 ? 0 : ($n != 0 ? 1 : 2); + } else if (strpos($header, "n==2")) { + return $n == 1 ? 0 : ($n == 2 ? 1 : 2); + } else if (strpos($header, "n==0")) { + return $n == 1 ? 0 : ($n == 0 || ($n % 100 > 0 && $n % 100 < 20) ? 1 : 2); + } else if (strpos($header, "n>=2")) { + return $n == 1 ? 0 : ($n >= 2 && $n <= 4 ? 1 : 2); + } else if (strpos($header, "10>=2")) { + return $n == 1 ? 0 : ($n % 10 >= 2 && $n % 10 <= 4 && ($n % 100 < 10 || $n % 100 >= 20) ? 1 : 2); + } + return $n % 10 == 1 ? 0 : ($n % 10 == 2 ? 1 : 2); + } else if (strpos($header, "plurals=4")) { + if (strpos($header, "100==2")) { + return $n % 100 == 1 ? 0 : ($n % 100 == 2 ? 1 : ($n % 100 == 3 || $n % 100 == 4 ? 2 : 3)); + } else if (strpos($header, "n>=3")) { + return $n == 1 ? 0 : ($n == 2 ? 1 : ($n == 0 || ($n >= 3 && $n <= 10) ? 2 : 3)); + } else if (strpos($header, "100>=1")) { + return $n == 1 ? 0 : ($n == 0 || ($n % 100 >= 1 && $n % 100 <= 10) ? 1 : ($n % 100 >= 11 && $n % 100 <= 20 ? 2 : 3)); + } + } else if (strpos($header, "plurals=5")) { + return $n == 1 ? 0 : ($n == 2 ? 1 : ($n >= 3 && $n <= 6 ? 2 : ($n >= 7 && $n <= 10 ? 3 : 4))); + } else if (strpos($header, "plurals=6")) { + return $n == 0 ? 0 : + ($n == 1 ? 1 : + ($n == 2 ? 2 : + ($n % 100 >= 3 && $n % 100 <= 10 ? 3 : + ($n % 100 >= 11 ? 4 : 5)))); + } + + return 0; + } + + /** + * Clears the domains internal data array. Useful for testing i18n. + * + * @return void + */ + public static function clear() + { + $self = I18n::getInstance(); + $self->_domains = []; + } + + /** + * Get the loaded domains cache. + * + * @return array + */ + public static function domains() + { + $self = I18n::getInstance(); + return $self->_domains; + } + + /** + * Puts the parameters in raw translated strings + * + * @param string $translated The raw translated string + * @param array $args The arguments to put in the translation + * @return string Translated string with arguments + */ + public static function insertArgs($translated, array $args) + { + $len = count($args); + if ($len === 0 || ($len === 1 && $args[0] === null)) { + return $translated; + } + + if (is_array($args[0])) { + $args = $args[0]; + } + + $translated = preg_replace('/(?_escape . 'x') { + $delimiter = $this->_escape . 'x'; + return implode('', array_map('chr', array_map('hexdec', array_filter(explode($delimiter, $string))))); + } + if (substr($string, 0, 2) === $this->_escape . 'd') { + $delimiter = $this->_escape . 'd'; + return implode('', array_map('chr', array_filter(explode($delimiter, $string)))); + } + if ($string[0] === $this->_escape && isset($string[1]) && is_numeric($string[1])) { + $delimiter = $this->_escape; + return implode('', array_map('chr', array_filter(explode($delimiter, $string)))); + } + if (substr($string, 0, 3) === 'U00') { + $delimiter = 'U00'; + return implode('', array_map('chr', array_map('hexdec', array_filter(explode($delimiter, $string))))); + } + if (preg_match('/U([0-9a-fA-F]{4})/', $string, $match)) { + return Multibyte::ascii([hexdec($match[1])]); + } + return $string; + } } diff --git a/lib/Cake/I18n/L10n.php b/lib/Cake/I18n/L10n.php index cdfdd747..f3b717b1 100755 --- a/lib/Cake/I18n/L10n.php +++ b/lib/Cake/I18n/L10n.php @@ -23,7 +23,8 @@ * * @package Cake.I18n */ -class L10n { +class L10n +{ /** * The language for current locale @@ -37,7 +38,7 @@ class L10n { * * @var array */ - public $languagePath = array('en_us', 'eng'); + public $languagePath = ['en_us', 'eng']; /** * ISO 639-3 for current locale @@ -86,94 +87,180 @@ class L10n { * * @var array */ - protected $_l10nMap = array( - /* Afrikaans */ 'afr' => 'af', - /* Albanian */ 'sqi' => 'sq', - /* Albanian - bibliographic */ 'alb' => 'sq', - /* Arabic */ 'ara' => 'ar', - /* Armenian/Armenia */ 'hye' => 'hy', - /* Basque */ 'eus' => 'eu', - /* Basque */ 'baq' => 'eu', - /* Tibetan */ 'bod' => 'bo', - /* Tibetan - bibliographic */ 'tib' => 'bo', - /* Bosnian */ 'bos' => 'bs', - /* Bulgarian */ 'bul' => 'bg', - /* Byelorussian */ 'bel' => 'be', - /* Catalan */ 'cat' => 'ca', - /* Chinese */ 'zho' => 'zh', - /* Chinese - bibliographic */ 'chi' => 'zh', - /* Croatian */ 'hrv' => 'hr', - /* Czech */ 'ces' => 'cs', - /* Czech - bibliographic */ 'cze' => 'cs', - /* Danish */ 'dan' => 'da', - /* Dutch (Standard) */ 'nld' => 'nl', - /* Dutch (Standard) - bibliographic */ 'dut' => 'nl', - /* English */ 'eng' => 'en', - /* Estonian */ 'est' => 'et', - /* Faeroese */ 'fao' => 'fo', - /* Farsi/Persian */ 'fas' => 'fa', - /* Farsi/Persian - bibliographic */ 'per' => 'fa', - /* Finnish */ 'fin' => 'fi', - /* French (Standard) */ 'fra' => 'fr', - /* French (Standard) - bibliographic */ 'fre' => 'fr', - /* Gaelic (Scots) */ 'gla' => 'gd', - /* Galician */ 'glg' => 'gl', - /* German (Standard) */ 'deu' => 'de', - /* German (Standard) - bibliographic */ 'ger' => 'de', - /* Greek */ 'gre' => 'el', - /* Greek */ 'ell' => 'el', - /* Hebrew */ 'heb' => 'he', - /* Hindi */ 'hin' => 'hi', - /* Hungarian */ 'hun' => 'hu', - /* Icelandic */ 'isl' => 'is', - /* Icelandic - bibliographic */ 'ice' => 'is', - /* Indonesian */ 'ind' => 'id', - /* Irish */ 'gle' => 'ga', - /* Italian */ 'ita' => 'it', - /* Japanese */ 'jpn' => 'ja', - /* Kazakh */ 'kaz' => 'kk', - /* Kalaallisut (Greenlandic) */ 'kal' => 'kl', - /* Korean */ 'kor' => 'ko', - /* Latvian */ 'lav' => 'lv', - /* Limburgish */ 'lim' => 'li', - /* Lithuanian */ 'lit' => 'lt', - /* Luxembourgish */ 'ltz' => 'lb', - /* Macedonian */ 'mkd' => 'mk', - /* Macedonian - bibliographic */ 'mac' => 'mk', - /* Malaysian */ 'msa' => 'ms', - /* Malaysian - bibliographic */ 'may' => 'ms', - /* Maltese */ 'mlt' => 'mt', - /* Norwegian */ 'nor' => 'no', - /* Norwegian Bokmal */ 'nob' => 'nb', - /* Norwegian Nynorsk */ 'nno' => 'nn', - /* Polish */ 'pol' => 'pl', - /* Portuguese (Portugal) */ 'por' => 'pt', - /* Rhaeto-Romanic */ 'roh' => 'rm', - /* Romanian */ 'ron' => 'ro', - /* Romanian - bibliographic */ 'rum' => 'ro', - /* Russian */ 'rus' => 'ru', - /* Sami */ 'sme' => 'se', - /* Serbian */ 'srp' => 'sr', - /* Slovak */ 'slk' => 'sk', - /* Slovak - bibliographic */ 'slo' => 'sk', - /* Slovenian */ 'slv' => 'sl', - /* Sorbian */ 'wen' => 'sb', - /* Spanish (Spain - Traditional) */ 'spa' => 'es', - /* Swedish */ 'swe' => 'sv', - /* Thai */ 'tha' => 'th', - /* Tsonga */ 'tso' => 'ts', - /* Tswana */ 'tsn' => 'tn', - /* Turkish */ 'tur' => 'tr', - /* Ukrainian */ 'ukr' => 'uk', - /* Urdu */ 'urd' => 'ur', - /* Venda */ 'ven' => 've', - /* Vietnamese */ 'vie' => 'vi', - /* Welsh */ 'cym' => 'cy', - /* Welsh - bibliographic */ 'wel' => 'cy', - /* Xhosa */ 'xho' => 'xh', - /* Yiddish */ 'yid' => 'yi', - /* Zulu */ 'zul' => 'zu' - ); + protected $_l10nMap = [ + /* Afrikaans */ + 'afr' => 'af', + /* Albanian */ + 'sqi' => 'sq', + /* Albanian - bibliographic */ + 'alb' => 'sq', + /* Arabic */ + 'ara' => 'ar', + /* Armenian/Armenia */ + 'hye' => 'hy', + /* Basque */ + 'eus' => 'eu', + /* Basque */ + 'baq' => 'eu', + /* Tibetan */ + 'bod' => 'bo', + /* Tibetan - bibliographic */ + 'tib' => 'bo', + /* Bosnian */ + 'bos' => 'bs', + /* Bulgarian */ + 'bul' => 'bg', + /* Byelorussian */ + 'bel' => 'be', + /* Catalan */ + 'cat' => 'ca', + /* Chinese */ + 'zho' => 'zh', + /* Chinese - bibliographic */ + 'chi' => 'zh', + /* Croatian */ + 'hrv' => 'hr', + /* Czech */ + 'ces' => 'cs', + /* Czech - bibliographic */ + 'cze' => 'cs', + /* Danish */ + 'dan' => 'da', + /* Dutch (Standard) */ + 'nld' => 'nl', + /* Dutch (Standard) - bibliographic */ + 'dut' => 'nl', + /* English */ + 'eng' => 'en', + /* Estonian */ + 'est' => 'et', + /* Faeroese */ + 'fao' => 'fo', + /* Farsi/Persian */ + 'fas' => 'fa', + /* Farsi/Persian - bibliographic */ + 'per' => 'fa', + /* Finnish */ + 'fin' => 'fi', + /* French (Standard) */ + 'fra' => 'fr', + /* French (Standard) - bibliographic */ + 'fre' => 'fr', + /* Gaelic (Scots) */ + 'gla' => 'gd', + /* Galician */ + 'glg' => 'gl', + /* German (Standard) */ + 'deu' => 'de', + /* German (Standard) - bibliographic */ + 'ger' => 'de', + /* Greek */ + 'gre' => 'el', + /* Greek */ + 'ell' => 'el', + /* Hebrew */ + 'heb' => 'he', + /* Hindi */ + 'hin' => 'hi', + /* Hungarian */ + 'hun' => 'hu', + /* Icelandic */ + 'isl' => 'is', + /* Icelandic - bibliographic */ + 'ice' => 'is', + /* Indonesian */ + 'ind' => 'id', + /* Irish */ + 'gle' => 'ga', + /* Italian */ + 'ita' => 'it', + /* Japanese */ + 'jpn' => 'ja', + /* Kazakh */ + 'kaz' => 'kk', + /* Kalaallisut (Greenlandic) */ + 'kal' => 'kl', + /* Korean */ + 'kor' => 'ko', + /* Latvian */ + 'lav' => 'lv', + /* Limburgish */ + 'lim' => 'li', + /* Lithuanian */ + 'lit' => 'lt', + /* Luxembourgish */ + 'ltz' => 'lb', + /* Macedonian */ + 'mkd' => 'mk', + /* Macedonian - bibliographic */ + 'mac' => 'mk', + /* Malaysian */ + 'msa' => 'ms', + /* Malaysian - bibliographic */ + 'may' => 'ms', + /* Maltese */ + 'mlt' => 'mt', + /* Norwegian */ + 'nor' => 'no', + /* Norwegian Bokmal */ + 'nob' => 'nb', + /* Norwegian Nynorsk */ + 'nno' => 'nn', + /* Polish */ + 'pol' => 'pl', + /* Portuguese (Portugal) */ + 'por' => 'pt', + /* Rhaeto-Romanic */ + 'roh' => 'rm', + /* Romanian */ + 'ron' => 'ro', + /* Romanian - bibliographic */ + 'rum' => 'ro', + /* Russian */ + 'rus' => 'ru', + /* Sami */ + 'sme' => 'se', + /* Serbian */ + 'srp' => 'sr', + /* Slovak */ + 'slk' => 'sk', + /* Slovak - bibliographic */ + 'slo' => 'sk', + /* Slovenian */ + 'slv' => 'sl', + /* Sorbian */ + 'wen' => 'sb', + /* Spanish (Spain - Traditional) */ + 'spa' => 'es', + /* Swedish */ + 'swe' => 'sv', + /* Thai */ + 'tha' => 'th', + /* Tsonga */ + 'tso' => 'ts', + /* Tswana */ + 'tsn' => 'tn', + /* Turkish */ + 'tur' => 'tr', + /* Ukrainian */ + 'ukr' => 'uk', + /* Urdu */ + 'urd' => 'ur', + /* Venda */ + 'ven' => 've', + /* Vietnamese */ + 'vie' => 'vi', + /* Welsh */ + 'cym' => 'cy', + /* Welsh - bibliographic */ + 'wel' => 'cy', + /* Xhosa */ + 'xho' => 'xh', + /* Yiddish */ + 'yid' => 'yi', + /* Zulu */ + 'zul' => 'zu' + ]; /** * HTTP_ACCEPT_LANGUAGE catalog @@ -182,170 +269,171 @@ class L10n { * * @var array */ - protected $_l10nCatalog = array( - 'af' => array('language' => 'Afrikaans', 'locale' => 'afr', 'localeFallback' => 'afr', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'ar' => array('language' => 'Arabic', 'locale' => 'ara', 'localeFallback' => 'ara', 'charset' => 'utf-8', 'direction' => 'rtl'), - 'ar-ae' => array('language' => 'Arabic (U.A.E.)', 'locale' => 'ar_ae', 'localeFallback' => 'ara', 'charset' => 'utf-8', 'direction' => 'rtl'), - 'ar-bh' => array('language' => 'Arabic (Bahrain)', 'locale' => 'ar_bh', 'localeFallback' => 'ara', 'charset' => 'utf-8', 'direction' => 'rtl'), - 'ar-dz' => array('language' => 'Arabic (Algeria)', 'locale' => 'ar_dz', 'localeFallback' => 'ara', 'charset' => 'utf-8', 'direction' => 'rtl'), - 'ar-eg' => array('language' => 'Arabic (Egypt)', 'locale' => 'ar_eg', 'localeFallback' => 'ara', 'charset' => 'utf-8', 'direction' => 'rtl'), - 'ar-iq' => array('language' => 'Arabic (Iraq)', 'locale' => 'ar_iq', 'localeFallback' => 'ara', 'charset' => 'utf-8', 'direction' => 'rtl'), - 'ar-jo' => array('language' => 'Arabic (Jordan)', 'locale' => 'ar_jo', 'localeFallback' => 'ara', 'charset' => 'utf-8', 'direction' => 'rtl'), - 'ar-kw' => array('language' => 'Arabic (Kuwait)', 'locale' => 'ar_kw', 'localeFallback' => 'ara', 'charset' => 'utf-8', 'direction' => 'rtl'), - 'ar-lb' => array('language' => 'Arabic (Lebanon)', 'locale' => 'ar_lb', 'localeFallback' => 'ara', 'charset' => 'utf-8', 'direction' => 'rtl'), - 'ar-ly' => array('language' => 'Arabic (Libya)', 'locale' => 'ar_ly', 'localeFallback' => 'ara', 'charset' => 'utf-8', 'direction' => 'rtl'), - 'ar-ma' => array('language' => 'Arabic (Morocco)', 'locale' => 'ar_ma', 'localeFallback' => 'ara', 'charset' => 'utf-8', 'direction' => 'rtl'), - 'ar-om' => array('language' => 'Arabic (Oman)', 'locale' => 'ar_om', 'localeFallback' => 'ara', 'charset' => 'utf-8', 'direction' => 'rtl'), - 'ar-qa' => array('language' => 'Arabic (Qatar)', 'locale' => 'ar_qa', 'localeFallback' => 'ara', 'charset' => 'utf-8', 'direction' => 'rtl'), - 'ar-sa' => array('language' => 'Arabic (Saudi Arabia)', 'locale' => 'ar_sa', 'localeFallback' => 'ara', 'charset' => 'utf-8', 'direction' => 'rtl'), - 'ar-sy' => array('language' => 'Arabic (Syria)', 'locale' => 'ar_sy', 'localeFallback' => 'ara', 'charset' => 'utf-8', 'direction' => 'rtl'), - 'ar-tn' => array('language' => 'Arabic (Tunisia)', 'locale' => 'ar_tn', 'localeFallback' => 'ara', 'charset' => 'utf-8', 'direction' => 'rtl'), - 'ar-ye' => array('language' => 'Arabic (Yemen)', 'locale' => 'ar_ye', 'localeFallback' => 'ara', 'charset' => 'utf-8', 'direction' => 'rtl'), - 'be' => array('language' => 'Byelorussian', 'locale' => 'bel', 'localeFallback' => 'bel', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'bg' => array('language' => 'Bulgarian', 'locale' => 'bul', 'localeFallback' => 'bul', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'bo' => array('language' => 'Tibetan', 'locale' => 'bod', 'localeFallback' => 'bod', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'bo-cn' => array('language' => 'Tibetan (China)', 'locale' => 'bo_cn', 'localeFallback' => 'bod', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'bo-in' => array('language' => 'Tibetan (India)', 'locale' => 'bo_in', 'localeFallback' => 'bod', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'bs' => array('language' => 'Bosnian', 'locale' => 'bos', 'localeFallback' => 'bos', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'ca' => array('language' => 'Catalan', 'locale' => 'cat', 'localeFallback' => 'cat', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'cs' => array('language' => 'Czech', 'locale' => 'ces', 'localeFallback' => 'ces', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'da' => array('language' => 'Danish', 'locale' => 'dan', 'localeFallback' => 'dan', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'da-dk' => array('language' => 'Danish (Denmark)', 'locale' => 'da_dk', 'localeFallback' => 'dan', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'de' => array('language' => 'German (Standard)', 'locale' => 'deu', 'localeFallback' => 'deu', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'de-at' => array('language' => 'German (Austria)', 'locale' => 'de_at', 'localeFallback' => 'deu', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'de-ch' => array('language' => 'German (Swiss)', 'locale' => 'de_ch', 'localeFallback' => 'deu', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'de-de' => array('language' => 'German (Germany)', 'locale' => 'de_de', 'localeFallback' => 'deu', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'de-li' => array('language' => 'German (Liechtenstein)', 'locale' => 'de_li', 'localeFallback' => 'deu', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'de-lu' => array('language' => 'German (Luxembourg)', 'locale' => 'de_lu', 'localeFallback' => 'deu', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'el' => array('language' => 'Greek', 'locale' => 'ell', 'localeFallback' => 'ell', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'en' => array('language' => 'English', 'locale' => 'eng', 'localeFallback' => 'eng', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'en-au' => array('language' => 'English (Australian)', 'locale' => 'en_au', 'localeFallback' => 'eng', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'en-bz' => array('language' => 'English (Belize)', 'locale' => 'en_bz', 'localeFallback' => 'eng', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'en-ca' => array('language' => 'English (Canadian)', 'locale' => 'en_ca', 'localeFallback' => 'eng', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'en-gb' => array('language' => 'English (British)', 'locale' => 'en_gb', 'localeFallback' => 'eng', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'en-ie' => array('language' => 'English (Ireland)', 'locale' => 'en_ie', 'localeFallback' => 'eng', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'en-jm' => array('language' => 'English (Jamaica)', 'locale' => 'en_jm', 'localeFallback' => 'eng', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'en-nz' => array('language' => 'English (New Zealand)', 'locale' => 'en_nz', 'localeFallback' => 'eng', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'en-tt' => array('language' => 'English (Trinidad)', 'locale' => 'en_tt', 'localeFallback' => 'eng', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'en-us' => array('language' => 'English (United States)', 'locale' => 'en_us', 'localeFallback' => 'eng', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'en-za' => array('language' => 'English (South Africa)', 'locale' => 'en_za', 'localeFallback' => 'eng', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'es' => array('language' => 'Spanish (Spain - Traditional)', 'locale' => 'spa', 'localeFallback' => 'spa', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'es-ar' => array('language' => 'Spanish (Argentina)', 'locale' => 'es_ar', 'localeFallback' => 'spa', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'es-bo' => array('language' => 'Spanish (Bolivia)', 'locale' => 'es_bo', 'localeFallback' => 'spa', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'es-cl' => array('language' => 'Spanish (Chile)', 'locale' => 'es_cl', 'localeFallback' => 'spa', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'es-co' => array('language' => 'Spanish (Colombia)', 'locale' => 'es_co', 'localeFallback' => 'spa', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'es-cr' => array('language' => 'Spanish (Costa Rica)', 'locale' => 'es_cr', 'localeFallback' => 'spa', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'es-do' => array('language' => 'Spanish (Dominican Republic)', 'locale' => 'es_do', 'localeFallback' => 'spa', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'es-ec' => array('language' => 'Spanish (Ecuador)', 'locale' => 'es_ec', 'localeFallback' => 'spa', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'es-es' => array('language' => 'Spanish (Spain)', 'locale' => 'es_es', 'localeFallback' => 'spa', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'es-gt' => array('language' => 'Spanish (Guatemala)', 'locale' => 'es_gt', 'localeFallback' => 'spa', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'es-hn' => array('language' => 'Spanish (Honduras)', 'locale' => 'es_hn', 'localeFallback' => 'spa', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'es-mx' => array('language' => 'Spanish (Mexican)', 'locale' => 'es_mx', 'localeFallback' => 'spa', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'es-ni' => array('language' => 'Spanish (Nicaragua)', 'locale' => 'es_ni', 'localeFallback' => 'spa', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'es-pa' => array('language' => 'Spanish (Panama)', 'locale' => 'es_pa', 'localeFallback' => 'spa', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'es-pe' => array('language' => 'Spanish (Peru)', 'locale' => 'es_pe', 'localeFallback' => 'spa', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'es-pr' => array('language' => 'Spanish (Puerto Rico)', 'locale' => 'es_pr', 'localeFallback' => 'spa', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'es-py' => array('language' => 'Spanish (Paraguay)', 'locale' => 'es_py', 'localeFallback' => 'spa', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'es-sv' => array('language' => 'Spanish (El Salvador)', 'locale' => 'es_sv', 'localeFallback' => 'spa', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'es-uy' => array('language' => 'Spanish (Uruguay)', 'locale' => 'es_uy', 'localeFallback' => 'spa', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'es-ve' => array('language' => 'Spanish (Venezuela)', 'locale' => 'es_ve', 'localeFallback' => 'spa', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'et' => array('language' => 'Estonian', 'locale' => 'est', 'localeFallback' => 'est', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'et-ee' => array('language' => 'Estonian (Estonia)', 'locale' => 'et_ee', 'localeFallback' => 'est', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'eu' => array('language' => 'Basque', 'locale' => 'eus', 'localeFallback' => 'eus', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'fa' => array('language' => 'Farsi', 'locale' => 'fas', 'localeFallback' => 'fas', 'charset' => 'utf-8', 'direction' => 'rtl'), - 'fi' => array('language' => 'Finnish', 'locale' => 'fin', 'localeFallback' => 'fin', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'fi-fi' => array('language' => 'Finnish (Finland)', 'locale' => 'fi_fi', 'localeFallback' => 'fin', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'fo' => array('language' => 'Faeroese', 'locale' => 'fao', 'localeFallback' => 'fao', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'fo-fo' => array('language' => 'Faeroese (Faroe Island)', 'locale' => 'fo_fo', 'localeFallback' => 'fao', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'fr' => array('language' => 'French (Standard)', 'locale' => 'fra', 'localeFallback' => 'fra', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'fr-be' => array('language' => 'French (Belgium)', 'locale' => 'fr_be', 'localeFallback' => 'fra', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'fr-ca' => array('language' => 'French (Canadian)', 'locale' => 'fr_ca', 'localeFallback' => 'fra', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'fr-ch' => array('language' => 'French (Swiss)', 'locale' => 'fr_ch', 'localeFallback' => 'fra', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'fr-fr' => array('language' => 'French (France)', 'locale' => 'fr_fr', 'localeFallback' => 'fra', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'fr-lu' => array('language' => 'French (Luxembourg)', 'locale' => 'fr_lu', 'localeFallback' => 'fra', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'ga' => array('language' => 'Irish', 'locale' => 'gle', 'localeFallback' => 'gle', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'gd' => array('language' => 'Gaelic (Scots)', 'locale' => 'gla', 'localeFallback' => 'gla', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'gd-ie' => array('language' => 'Gaelic (Irish)', 'locale' => 'gd_ie', 'localeFallback' => 'gla', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'gl' => array('language' => 'Galician', 'locale' => 'glg', 'localeFallback' => 'glg', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'he' => array('language' => 'Hebrew', 'locale' => 'heb', 'localeFallback' => 'heb', 'charset' => 'utf-8', 'direction' => 'rtl'), - 'hi' => array('language' => 'Hindi', 'locale' => 'hin', 'localeFallback' => 'hin', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'hr' => array('language' => 'Croatian', 'locale' => 'hrv', 'localeFallback' => 'hrv', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'hu' => array('language' => 'Hungarian', 'locale' => 'hun', 'localeFallback' => 'hun', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'hu-hu' => array('language' => 'Hungarian (Hungary)', 'locale' => 'hu_hu', 'localeFallback' => 'hun', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'hy' => array('language' => 'Armenian - Armenia', 'locale' => 'hye', 'localeFallback' => 'hye', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'id' => array('language' => 'Indonesian', 'locale' => 'ind', 'localeFallback' => 'ind', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'is' => array('language' => 'Icelandic', 'locale' => 'isl', 'localeFallback' => 'isl', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'is-is' => array('language' => 'Icelandic (Iceland)', 'locale' => 'is_is', 'localeFallback' => 'isl', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'it' => array('language' => 'Italian', 'locale' => 'ita', 'localeFallback' => 'ita', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'it-ch' => array('language' => 'Italian (Swiss) ', 'locale' => 'it_ch', 'localeFallback' => 'ita', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'ja' => array('language' => 'Japanese', 'locale' => 'jpn', 'localeFallback' => 'jpn', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'kk' => array('language' => 'Kazakh', 'locale' => 'kaz', 'localeFallback' => 'kaz', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'kl' => array('language' => 'Kalaallisut (Greenlandic)', 'locale' => 'kal', 'localeFallback' => 'kal', 'charset' => 'kl', 'direction' => 'ltr'), - 'kl-gl' => array('language' => 'Kalaallisut (Greenland)', 'locale' => 'kl_gl', 'localeFallback' => 'kal', 'charset' => 'kl', 'direction' => 'ltr'), - 'ko' => array('language' => 'Korean', 'locale' => 'kor', 'localeFallback' => 'kor', 'charset' => 'kr', 'direction' => 'ltr'), - 'ko-kp' => array('language' => 'Korea (North)', 'locale' => 'ko_kp', 'localeFallback' => 'kor', 'charset' => 'kr', 'direction' => 'ltr'), - 'ko-kr' => array('language' => 'Korea (South)', 'locale' => 'ko_kr', 'localeFallback' => 'kor', 'charset' => 'kr', 'direction' => 'ltr'), - 'koi8-r' => array('language' => 'Russian', 'locale' => 'koi8_r', 'localeFallback' => 'rus', 'charset' => 'koi8-r', 'direction' => 'ltr'), - 'lb' => array('language' => 'Luxembourgish', 'locale' => 'ltz', 'localeFallback' => 'ltz', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'li' => array('language' => 'Limburgish', 'locale' => 'lim', 'localeFallback' => 'nld', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'lt' => array('language' => 'Lithuanian', 'locale' => 'lit', 'localeFallback' => 'lit', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'lv' => array('language' => 'Latvian', 'locale' => 'lav', 'localeFallback' => 'lav', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'lv-lv' => array('language' => 'Latvian (Latvia)', 'locale' => 'lv_lv', 'localeFallback' => 'lav', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'mk' => array('language' => 'FYRO Macedonian', 'locale' => 'mkd', 'localeFallback' => 'mkd', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'mk-mk' => array('language' => 'Macedonian', 'locale' => 'mk_mk', 'localeFallback' => 'mkd', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'ms' => array('language' => 'Malaysian', 'locale' => 'msa', 'localeFallback' => 'msa', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'mt' => array('language' => 'Maltese', 'locale' => 'mlt', 'localeFallback' => 'mlt', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'nb' => array('language' => 'Norwegian Bokmal', 'locale' => 'nob', 'localeFallback' => 'nor', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'nb-no' => array('language' => 'Norwegian Bokmål (Norway)', 'locale' => 'nb_no', 'localeFallback' => 'nor', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'nl' => array('language' => 'Dutch (Standard)', 'locale' => 'nld', 'localeFallback' => 'nld', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'nl-be' => array('language' => 'Dutch (Belgium)', 'locale' => 'nl_be', 'localeFallback' => 'nld', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'nl-nl' => array('language' => 'Dutch (Netherlands)', 'locale' => 'nl_nl', 'localeFallback' => 'nld', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'nn' => array('language' => 'Norwegian Nynorsk', 'locale' => 'nno', 'localeFallback' => 'nor', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'nn-no' => array('language' => 'Norwegian Nynorsk (Norway)', 'locale' => 'nn_no', 'localeFallback' => 'nor', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'no' => array('language' => 'Norwegian', 'locale' => 'nor', 'localeFallback' => 'nor', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'pl' => array('language' => 'Polish', 'locale' => 'pol', 'localeFallback' => 'pol', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'pl-pl' => array('language' => 'Polish (Poland)', 'locale' => 'pl_pl', 'localeFallback' => 'pol', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'pt' => array('language' => 'Portuguese (Portugal)', 'locale' => 'por', 'localeFallback' => 'por', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'pt-br' => array('language' => 'Portuguese (Brazil)', 'locale' => 'pt_br', 'localeFallback' => 'por', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'rm' => array('language' => 'Rhaeto-Romanic', 'locale' => 'roh', 'localeFallback' => 'roh', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'ro' => array('language' => 'Romanian', 'locale' => 'ron', 'localeFallback' => 'ron', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'ro-mo' => array('language' => 'Romanian (Moldavia)', 'locale' => 'ro_mo', 'localeFallback' => 'ron', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'ro-ro' => array('language' => 'Romanian (Romania)', 'locale' => 'ro_ro', 'localeFallback' => 'ron', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'ru' => array('language' => 'Russian', 'locale' => 'rus', 'localeFallback' => 'rus', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'ru-mo' => array('language' => 'Russian (Moldavia)', 'locale' => 'ru_mo', 'localeFallback' => 'rus', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'ru-ru' => array('language' => 'Russian (Russia)', 'locale' => 'ru_ru', 'localeFallback' => 'rus', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'sb' => array('language' => 'Sorbian', 'locale' => 'wen', 'localeFallback' => 'wen', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'sk' => array('language' => 'Slovak', 'locale' => 'slk', 'localeFallback' => 'slk', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'sl' => array('language' => 'Slovenian', 'locale' => 'slv', 'localeFallback' => 'slv', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'sq' => array('language' => 'Albanian', 'locale' => 'sqi', 'localeFallback' => 'sqi', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'sr' => array('language' => 'Serbian', 'locale' => 'srp', 'localeFallback' => 'srp', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'sv' => array('language' => 'Swedish', 'locale' => 'swe', 'localeFallback' => 'swe', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'sv-se' => array('language' => 'Swedish (Sweden)', 'locale' => 'sv_se', 'localeFallback' => 'swe', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'sv-fi' => array('language' => 'Swedish (Finland)', 'locale' => 'sv_fi', 'localeFallback' => 'swe', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'se' => array('language' => 'Sami', 'locale' => 'sme', 'localeFallback' => 'sme', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'th' => array('language' => 'Thai', 'locale' => 'tha', 'localeFallback' => 'tha', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'tn' => array('language' => 'Tswana', 'locale' => 'tsn', 'localeFallback' => 'tsn', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'tr' => array('language' => 'Turkish', 'locale' => 'tur', 'localeFallback' => 'tur', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'ts' => array('language' => 'Tsonga', 'locale' => 'tso', 'localeFallback' => 'tso', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'uk' => array('language' => 'Ukrainian', 'locale' => 'ukr', 'localeFallback' => 'ukr', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'ur' => array('language' => 'Urdu', 'locale' => 'urd', 'localeFallback' => 'urd', 'charset' => 'utf-8', 'direction' => 'rtl'), - 've' => array('language' => 'Venda', 'locale' => 'ven', 'localeFallback' => 'ven', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'vi' => array('language' => 'Vietnamese', 'locale' => 'vie', 'localeFallback' => 'vie', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'cy' => array('language' => 'Welsh', 'locale' => 'cym', 'localeFallback' => 'cym', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'xh' => array('language' => 'Xhosa', 'locale' => 'xho', 'localeFallback' => 'xho', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'yi' => array('language' => 'Yiddish', 'locale' => 'yid', 'localeFallback' => 'yid', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'zh' => array('language' => 'Chinese', 'locale' => 'zho', 'localeFallback' => 'zho', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'zh-cn' => array('language' => 'Chinese (PRC)', 'locale' => 'zh_cn', 'localeFallback' => 'zho', 'charset' => 'GB2312', 'direction' => 'ltr'), - 'zh-hk' => array('language' => 'Chinese (Hong Kong)', 'locale' => 'zh_hk', 'localeFallback' => 'zho', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'zh-sg' => array('language' => 'Chinese (Singapore)', 'locale' => 'zh_sg', 'localeFallback' => 'zho', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'zh-tw' => array('language' => 'Chinese (Taiwan)', 'locale' => 'zh_tw', 'localeFallback' => 'zho', 'charset' => 'utf-8', 'direction' => 'ltr'), - 'zu' => array('language' => 'Zulu', 'locale' => 'zul', 'localeFallback' => 'zul', 'charset' => 'utf-8', 'direction' => 'ltr') - ); + protected $_l10nCatalog = [ + 'af' => ['language' => 'Afrikaans', 'locale' => 'afr', 'localeFallback' => 'afr', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'ar' => ['language' => 'Arabic', 'locale' => 'ara', 'localeFallback' => 'ara', 'charset' => 'utf-8', 'direction' => 'rtl'], + 'ar-ae' => ['language' => 'Arabic (U.A.E.)', 'locale' => 'ar_ae', 'localeFallback' => 'ara', 'charset' => 'utf-8', 'direction' => 'rtl'], + 'ar-bh' => ['language' => 'Arabic (Bahrain)', 'locale' => 'ar_bh', 'localeFallback' => 'ara', 'charset' => 'utf-8', 'direction' => 'rtl'], + 'ar-dz' => ['language' => 'Arabic (Algeria)', 'locale' => 'ar_dz', 'localeFallback' => 'ara', 'charset' => 'utf-8', 'direction' => 'rtl'], + 'ar-eg' => ['language' => 'Arabic (Egypt)', 'locale' => 'ar_eg', 'localeFallback' => 'ara', 'charset' => 'utf-8', 'direction' => 'rtl'], + 'ar-iq' => ['language' => 'Arabic (Iraq)', 'locale' => 'ar_iq', 'localeFallback' => 'ara', 'charset' => 'utf-8', 'direction' => 'rtl'], + 'ar-jo' => ['language' => 'Arabic (Jordan)', 'locale' => 'ar_jo', 'localeFallback' => 'ara', 'charset' => 'utf-8', 'direction' => 'rtl'], + 'ar-kw' => ['language' => 'Arabic (Kuwait)', 'locale' => 'ar_kw', 'localeFallback' => 'ara', 'charset' => 'utf-8', 'direction' => 'rtl'], + 'ar-lb' => ['language' => 'Arabic (Lebanon)', 'locale' => 'ar_lb', 'localeFallback' => 'ara', 'charset' => 'utf-8', 'direction' => 'rtl'], + 'ar-ly' => ['language' => 'Arabic (Libya)', 'locale' => 'ar_ly', 'localeFallback' => 'ara', 'charset' => 'utf-8', 'direction' => 'rtl'], + 'ar-ma' => ['language' => 'Arabic (Morocco)', 'locale' => 'ar_ma', 'localeFallback' => 'ara', 'charset' => 'utf-8', 'direction' => 'rtl'], + 'ar-om' => ['language' => 'Arabic (Oman)', 'locale' => 'ar_om', 'localeFallback' => 'ara', 'charset' => 'utf-8', 'direction' => 'rtl'], + 'ar-qa' => ['language' => 'Arabic (Qatar)', 'locale' => 'ar_qa', 'localeFallback' => 'ara', 'charset' => 'utf-8', 'direction' => 'rtl'], + 'ar-sa' => ['language' => 'Arabic (Saudi Arabia)', 'locale' => 'ar_sa', 'localeFallback' => 'ara', 'charset' => 'utf-8', 'direction' => 'rtl'], + 'ar-sy' => ['language' => 'Arabic (Syria)', 'locale' => 'ar_sy', 'localeFallback' => 'ara', 'charset' => 'utf-8', 'direction' => 'rtl'], + 'ar-tn' => ['language' => 'Arabic (Tunisia)', 'locale' => 'ar_tn', 'localeFallback' => 'ara', 'charset' => 'utf-8', 'direction' => 'rtl'], + 'ar-ye' => ['language' => 'Arabic (Yemen)', 'locale' => 'ar_ye', 'localeFallback' => 'ara', 'charset' => 'utf-8', 'direction' => 'rtl'], + 'be' => ['language' => 'Byelorussian', 'locale' => 'bel', 'localeFallback' => 'bel', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'bg' => ['language' => 'Bulgarian', 'locale' => 'bul', 'localeFallback' => 'bul', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'bo' => ['language' => 'Tibetan', 'locale' => 'bod', 'localeFallback' => 'bod', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'bo-cn' => ['language' => 'Tibetan (China)', 'locale' => 'bo_cn', 'localeFallback' => 'bod', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'bo-in' => ['language' => 'Tibetan (India)', 'locale' => 'bo_in', 'localeFallback' => 'bod', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'bs' => ['language' => 'Bosnian', 'locale' => 'bos', 'localeFallback' => 'bos', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'ca' => ['language' => 'Catalan', 'locale' => 'cat', 'localeFallback' => 'cat', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'cs' => ['language' => 'Czech', 'locale' => 'ces', 'localeFallback' => 'ces', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'da' => ['language' => 'Danish', 'locale' => 'dan', 'localeFallback' => 'dan', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'da-dk' => ['language' => 'Danish (Denmark)', 'locale' => 'da_dk', 'localeFallback' => 'dan', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'de' => ['language' => 'German (Standard)', 'locale' => 'deu', 'localeFallback' => 'deu', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'de-at' => ['language' => 'German (Austria)', 'locale' => 'de_at', 'localeFallback' => 'deu', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'de-ch' => ['language' => 'German (Swiss)', 'locale' => 'de_ch', 'localeFallback' => 'deu', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'de-de' => ['language' => 'German (Germany)', 'locale' => 'de_de', 'localeFallback' => 'deu', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'de-li' => ['language' => 'German (Liechtenstein)', 'locale' => 'de_li', 'localeFallback' => 'deu', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'de-lu' => ['language' => 'German (Luxembourg)', 'locale' => 'de_lu', 'localeFallback' => 'deu', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'el' => ['language' => 'Greek', 'locale' => 'ell', 'localeFallback' => 'ell', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'en' => ['language' => 'English', 'locale' => 'eng', 'localeFallback' => 'eng', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'en-au' => ['language' => 'English (Australian)', 'locale' => 'en_au', 'localeFallback' => 'eng', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'en-bz' => ['language' => 'English (Belize)', 'locale' => 'en_bz', 'localeFallback' => 'eng', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'en-ca' => ['language' => 'English (Canadian)', 'locale' => 'en_ca', 'localeFallback' => 'eng', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'en-gb' => ['language' => 'English (British)', 'locale' => 'en_gb', 'localeFallback' => 'eng', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'en-ie' => ['language' => 'English (Ireland)', 'locale' => 'en_ie', 'localeFallback' => 'eng', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'en-jm' => ['language' => 'English (Jamaica)', 'locale' => 'en_jm', 'localeFallback' => 'eng', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'en-nz' => ['language' => 'English (New Zealand)', 'locale' => 'en_nz', 'localeFallback' => 'eng', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'en-tt' => ['language' => 'English (Trinidad)', 'locale' => 'en_tt', 'localeFallback' => 'eng', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'en-us' => ['language' => 'English (United States)', 'locale' => 'en_us', 'localeFallback' => 'eng', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'en-za' => ['language' => 'English (South Africa)', 'locale' => 'en_za', 'localeFallback' => 'eng', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'es' => ['language' => 'Spanish (Spain - Traditional)', 'locale' => 'spa', 'localeFallback' => 'spa', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'es-ar' => ['language' => 'Spanish (Argentina)', 'locale' => 'es_ar', 'localeFallback' => 'spa', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'es-bo' => ['language' => 'Spanish (Bolivia)', 'locale' => 'es_bo', 'localeFallback' => 'spa', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'es-cl' => ['language' => 'Spanish (Chile)', 'locale' => 'es_cl', 'localeFallback' => 'spa', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'es-co' => ['language' => 'Spanish (Colombia)', 'locale' => 'es_co', 'localeFallback' => 'spa', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'es-cr' => ['language' => 'Spanish (Costa Rica)', 'locale' => 'es_cr', 'localeFallback' => 'spa', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'es-do' => ['language' => 'Spanish (Dominican Republic)', 'locale' => 'es_do', 'localeFallback' => 'spa', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'es-ec' => ['language' => 'Spanish (Ecuador)', 'locale' => 'es_ec', 'localeFallback' => 'spa', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'es-es' => ['language' => 'Spanish (Spain)', 'locale' => 'es_es', 'localeFallback' => 'spa', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'es-gt' => ['language' => 'Spanish (Guatemala)', 'locale' => 'es_gt', 'localeFallback' => 'spa', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'es-hn' => ['language' => 'Spanish (Honduras)', 'locale' => 'es_hn', 'localeFallback' => 'spa', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'es-mx' => ['language' => 'Spanish (Mexican)', 'locale' => 'es_mx', 'localeFallback' => 'spa', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'es-ni' => ['language' => 'Spanish (Nicaragua)', 'locale' => 'es_ni', 'localeFallback' => 'spa', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'es-pa' => ['language' => 'Spanish (Panama)', 'locale' => 'es_pa', 'localeFallback' => 'spa', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'es-pe' => ['language' => 'Spanish (Peru)', 'locale' => 'es_pe', 'localeFallback' => 'spa', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'es-pr' => ['language' => 'Spanish (Puerto Rico)', 'locale' => 'es_pr', 'localeFallback' => 'spa', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'es-py' => ['language' => 'Spanish (Paraguay)', 'locale' => 'es_py', 'localeFallback' => 'spa', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'es-sv' => ['language' => 'Spanish (El Salvador)', 'locale' => 'es_sv', 'localeFallback' => 'spa', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'es-uy' => ['language' => 'Spanish (Uruguay)', 'locale' => 'es_uy', 'localeFallback' => 'spa', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'es-ve' => ['language' => 'Spanish (Venezuela)', 'locale' => 'es_ve', 'localeFallback' => 'spa', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'et' => ['language' => 'Estonian', 'locale' => 'est', 'localeFallback' => 'est', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'et-ee' => ['language' => 'Estonian (Estonia)', 'locale' => 'et_ee', 'localeFallback' => 'est', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'eu' => ['language' => 'Basque', 'locale' => 'eus', 'localeFallback' => 'eus', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'fa' => ['language' => 'Farsi', 'locale' => 'fas', 'localeFallback' => 'fas', 'charset' => 'utf-8', 'direction' => 'rtl'], + 'fi' => ['language' => 'Finnish', 'locale' => 'fin', 'localeFallback' => 'fin', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'fi-fi' => ['language' => 'Finnish (Finland)', 'locale' => 'fi_fi', 'localeFallback' => 'fin', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'fo' => ['language' => 'Faeroese', 'locale' => 'fao', 'localeFallback' => 'fao', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'fo-fo' => ['language' => 'Faeroese (Faroe Island)', 'locale' => 'fo_fo', 'localeFallback' => 'fao', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'fr' => ['language' => 'French (Standard)', 'locale' => 'fra', 'localeFallback' => 'fra', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'fr-be' => ['language' => 'French (Belgium)', 'locale' => 'fr_be', 'localeFallback' => 'fra', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'fr-ca' => ['language' => 'French (Canadian)', 'locale' => 'fr_ca', 'localeFallback' => 'fra', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'fr-ch' => ['language' => 'French (Swiss)', 'locale' => 'fr_ch', 'localeFallback' => 'fra', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'fr-fr' => ['language' => 'French (France)', 'locale' => 'fr_fr', 'localeFallback' => 'fra', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'fr-lu' => ['language' => 'French (Luxembourg)', 'locale' => 'fr_lu', 'localeFallback' => 'fra', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'ga' => ['language' => 'Irish', 'locale' => 'gle', 'localeFallback' => 'gle', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'gd' => ['language' => 'Gaelic (Scots)', 'locale' => 'gla', 'localeFallback' => 'gla', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'gd-ie' => ['language' => 'Gaelic (Irish)', 'locale' => 'gd_ie', 'localeFallback' => 'gla', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'gl' => ['language' => 'Galician', 'locale' => 'glg', 'localeFallback' => 'glg', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'he' => ['language' => 'Hebrew', 'locale' => 'heb', 'localeFallback' => 'heb', 'charset' => 'utf-8', 'direction' => 'rtl'], + 'hi' => ['language' => 'Hindi', 'locale' => 'hin', 'localeFallback' => 'hin', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'hr' => ['language' => 'Croatian', 'locale' => 'hrv', 'localeFallback' => 'hrv', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'hu' => ['language' => 'Hungarian', 'locale' => 'hun', 'localeFallback' => 'hun', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'hu-hu' => ['language' => 'Hungarian (Hungary)', 'locale' => 'hu_hu', 'localeFallback' => 'hun', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'hy' => ['language' => 'Armenian - Armenia', 'locale' => 'hye', 'localeFallback' => 'hye', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'id' => ['language' => 'Indonesian', 'locale' => 'ind', 'localeFallback' => 'ind', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'is' => ['language' => 'Icelandic', 'locale' => 'isl', 'localeFallback' => 'isl', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'is-is' => ['language' => 'Icelandic (Iceland)', 'locale' => 'is_is', 'localeFallback' => 'isl', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'it' => ['language' => 'Italian', 'locale' => 'ita', 'localeFallback' => 'ita', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'it-ch' => ['language' => 'Italian (Swiss) ', 'locale' => 'it_ch', 'localeFallback' => 'ita', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'ja' => ['language' => 'Japanese', 'locale' => 'jpn', 'localeFallback' => 'jpn', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'kk' => ['language' => 'Kazakh', 'locale' => 'kaz', 'localeFallback' => 'kaz', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'kl' => ['language' => 'Kalaallisut (Greenlandic)', 'locale' => 'kal', 'localeFallback' => 'kal', 'charset' => 'kl', 'direction' => 'ltr'], + 'kl-gl' => ['language' => 'Kalaallisut (Greenland)', 'locale' => 'kl_gl', 'localeFallback' => 'kal', 'charset' => 'kl', 'direction' => 'ltr'], + 'ko' => ['language' => 'Korean', 'locale' => 'kor', 'localeFallback' => 'kor', 'charset' => 'kr', 'direction' => 'ltr'], + 'ko-kp' => ['language' => 'Korea (North)', 'locale' => 'ko_kp', 'localeFallback' => 'kor', 'charset' => 'kr', 'direction' => 'ltr'], + 'ko-kr' => ['language' => 'Korea (South)', 'locale' => 'ko_kr', 'localeFallback' => 'kor', 'charset' => 'kr', 'direction' => 'ltr'], + 'koi8-r' => ['language' => 'Russian', 'locale' => 'koi8_r', 'localeFallback' => 'rus', 'charset' => 'koi8-r', 'direction' => 'ltr'], + 'lb' => ['language' => 'Luxembourgish', 'locale' => 'ltz', 'localeFallback' => 'ltz', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'li' => ['language' => 'Limburgish', 'locale' => 'lim', 'localeFallback' => 'nld', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'lt' => ['language' => 'Lithuanian', 'locale' => 'lit', 'localeFallback' => 'lit', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'lv' => ['language' => 'Latvian', 'locale' => 'lav', 'localeFallback' => 'lav', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'lv-lv' => ['language' => 'Latvian (Latvia)', 'locale' => 'lv_lv', 'localeFallback' => 'lav', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'mk' => ['language' => 'FYRO Macedonian', 'locale' => 'mkd', 'localeFallback' => 'mkd', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'mk-mk' => ['language' => 'Macedonian', 'locale' => 'mk_mk', 'localeFallback' => 'mkd', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'ms' => ['language' => 'Malaysian', 'locale' => 'msa', 'localeFallback' => 'msa', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'mt' => ['language' => 'Maltese', 'locale' => 'mlt', 'localeFallback' => 'mlt', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'nb' => ['language' => 'Norwegian Bokmal', 'locale' => 'nob', 'localeFallback' => 'nor', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'nb-no' => ['language' => 'Norwegian Bokmål (Norway)', 'locale' => 'nb_no', 'localeFallback' => 'nor', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'nl' => ['language' => 'Dutch (Standard)', 'locale' => 'nld', 'localeFallback' => 'nld', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'nl-be' => ['language' => 'Dutch (Belgium)', 'locale' => 'nl_be', 'localeFallback' => 'nld', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'nl-nl' => ['language' => 'Dutch (Netherlands)', 'locale' => 'nl_nl', 'localeFallback' => 'nld', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'nn' => ['language' => 'Norwegian Nynorsk', 'locale' => 'nno', 'localeFallback' => 'nor', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'nn-no' => ['language' => 'Norwegian Nynorsk (Norway)', 'locale' => 'nn_no', 'localeFallback' => 'nor', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'no' => ['language' => 'Norwegian', 'locale' => 'nor', 'localeFallback' => 'nor', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'pl' => ['language' => 'Polish', 'locale' => 'pol', 'localeFallback' => 'pol', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'pl-pl' => ['language' => 'Polish (Poland)', 'locale' => 'pl_pl', 'localeFallback' => 'pol', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'pt' => ['language' => 'Portuguese (Portugal)', 'locale' => 'por', 'localeFallback' => 'por', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'pt-br' => ['language' => 'Portuguese (Brazil)', 'locale' => 'pt_br', 'localeFallback' => 'por', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'rm' => ['language' => 'Rhaeto-Romanic', 'locale' => 'roh', 'localeFallback' => 'roh', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'ro' => ['language' => 'Romanian', 'locale' => 'ron', 'localeFallback' => 'ron', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'ro-mo' => ['language' => 'Romanian (Moldavia)', 'locale' => 'ro_mo', 'localeFallback' => 'ron', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'ro-ro' => ['language' => 'Romanian (Romania)', 'locale' => 'ro_ro', 'localeFallback' => 'ron', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'ru' => ['language' => 'Russian', 'locale' => 'rus', 'localeFallback' => 'rus', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'ru-mo' => ['language' => 'Russian (Moldavia)', 'locale' => 'ru_mo', 'localeFallback' => 'rus', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'ru-ru' => ['language' => 'Russian (Russia)', 'locale' => 'ru_ru', 'localeFallback' => 'rus', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'sb' => ['language' => 'Sorbian', 'locale' => 'wen', 'localeFallback' => 'wen', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'sk' => ['language' => 'Slovak', 'locale' => 'slk', 'localeFallback' => 'slk', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'sl' => ['language' => 'Slovenian', 'locale' => 'slv', 'localeFallback' => 'slv', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'sq' => ['language' => 'Albanian', 'locale' => 'sqi', 'localeFallback' => 'sqi', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'sr' => ['language' => 'Serbian', 'locale' => 'srp', 'localeFallback' => 'srp', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'sv' => ['language' => 'Swedish', 'locale' => 'swe', 'localeFallback' => 'swe', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'sv-se' => ['language' => 'Swedish (Sweden)', 'locale' => 'sv_se', 'localeFallback' => 'swe', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'sv-fi' => ['language' => 'Swedish (Finland)', 'locale' => 'sv_fi', 'localeFallback' => 'swe', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'se' => ['language' => 'Sami', 'locale' => 'sme', 'localeFallback' => 'sme', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'th' => ['language' => 'Thai', 'locale' => 'tha', 'localeFallback' => 'tha', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'tn' => ['language' => 'Tswana', 'locale' => 'tsn', 'localeFallback' => 'tsn', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'tr' => ['language' => 'Turkish', 'locale' => 'tur', 'localeFallback' => 'tur', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'ts' => ['language' => 'Tsonga', 'locale' => 'tso', 'localeFallback' => 'tso', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'uk' => ['language' => 'Ukrainian', 'locale' => 'ukr', 'localeFallback' => 'ukr', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'ur' => ['language' => 'Urdu', 'locale' => 'urd', 'localeFallback' => 'urd', 'charset' => 'utf-8', 'direction' => 'rtl'], + 've' => ['language' => 'Venda', 'locale' => 'ven', 'localeFallback' => 'ven', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'vi' => ['language' => 'Vietnamese', 'locale' => 'vie', 'localeFallback' => 'vie', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'cy' => ['language' => 'Welsh', 'locale' => 'cym', 'localeFallback' => 'cym', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'xh' => ['language' => 'Xhosa', 'locale' => 'xho', 'localeFallback' => 'xho', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'yi' => ['language' => 'Yiddish', 'locale' => 'yid', 'localeFallback' => 'yid', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'zh' => ['language' => 'Chinese', 'locale' => 'zho', 'localeFallback' => 'zho', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'zh-cn' => ['language' => 'Chinese (PRC)', 'locale' => 'zh_cn', 'localeFallback' => 'zho', 'charset' => 'GB2312', 'direction' => 'ltr'], + 'zh-hk' => ['language' => 'Chinese (Hong Kong)', 'locale' => 'zh_hk', 'localeFallback' => 'zho', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'zh-sg' => ['language' => 'Chinese (Singapore)', 'locale' => 'zh_sg', 'localeFallback' => 'zho', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'zh-tw' => ['language' => 'Chinese (Taiwan)', 'locale' => 'zh_tw', 'localeFallback' => 'zho', 'charset' => 'utf-8', 'direction' => 'ltr'], + 'zu' => ['language' => 'Zulu', 'locale' => 'zul', 'localeFallback' => 'zul', 'charset' => 'utf-8', 'direction' => 'ltr'] + ]; /** * Class constructor */ - public function __construct() { + public function __construct() + { if (defined('DEFAULT_LANGUAGE')) { $this->default = DEFAULT_LANGUAGE; } @@ -363,7 +451,8 @@ public function __construct() { * @param string $language Language (if null will use DEFAULT_LANGUAGE if defined) * @return mixed */ - public function get($language = null) { + public function get($language = null) + { if ($language !== null) { return $this->_setLanguage($language); } @@ -381,7 +470,8 @@ public function get($language = null) { * @param string $language Language (if null will use L10n::$default if defined) * @return mixed */ - protected function _setLanguage($language = null) { + protected function _setLanguage($language = null) + { $catalog = false; if ($language !== null) { $catalog = $this->catalog($language); @@ -394,17 +484,17 @@ protected function _setLanguage($language = null) { if ($catalog) { $this->language = $catalog['language']; - $this->languagePath = array_unique(array( + $this->languagePath = array_unique([ $catalog['locale'], $catalog['localeFallback'] - )); + ]); $this->lang = $language; $this->locale = $catalog['locale']; $this->charset = $catalog['charset']; $this->direction = $catalog['direction']; - } elseif ($language) { + } else if ($language) { $this->lang = $language; - $this->languagePath = array($language); + $this->languagePath = [$language]; } if ($this->default && $language !== $this->default) { @@ -424,12 +514,43 @@ protected function _setLanguage($language = null) { } } + /** + * Attempts to find catalog record for requested language + * + * @param string|array $language string requested language, array of requested languages, or null for whole catalog + * @return array|bool array catalog record for requested language, array of catalog records, whole catalog, + * or false when language doesn't exist + */ + public function catalog($language = null) + { + if (is_array($language)) { + $result = []; + foreach ($language as $_language) { + if ($_result = $this->catalog($_language)) { + $result[$_language] = $_result; + } + } + return $result; + } + if (is_string($language)) { + if (isset($this->_l10nCatalog[$language])) { + return $this->_l10nCatalog[$language]; + } + if (isset($this->_l10nMap[$language]) && isset($this->_l10nCatalog[$this->_l10nMap[$language]])) { + return $this->_l10nCatalog[$this->_l10nMap[$language]]; + } + return false; + } + return $this->_l10nCatalog; + } + /** * Attempts to find the locale settings based on the HTTP_ACCEPT_LANGUAGE variable * * @return bool Success */ - protected function _autoLanguage() { + protected function _autoLanguage() + { $_detectableLanguages = CakeRequest::acceptLanguage(); foreach ($_detectableLanguages as $langKey) { if (isset($this->_l10nCatalog[$langKey])) { @@ -454,9 +575,10 @@ protected function _autoLanguage() { * @return string|array|bool string language/locale, array of those values, whole map as an array, * or false when language/locale doesn't exist */ - public function map($mixed = null) { + public function map($mixed = null) + { if (is_array($mixed)) { - $result = array(); + $result = []; foreach ($mixed as $_mixed) { if ($_result = $this->map($_mixed)) { $result[$_mixed] = $_result; @@ -476,33 +598,4 @@ public function map($mixed = null) { return $this->_l10nMap; } - /** - * Attempts to find catalog record for requested language - * - * @param string|array $language string requested language, array of requested languages, or null for whole catalog - * @return array|bool array catalog record for requested language, array of catalog records, whole catalog, - * or false when language doesn't exist - */ - public function catalog($language = null) { - if (is_array($language)) { - $result = array(); - foreach ($language as $_language) { - if ($_result = $this->catalog($_language)) { - $result[$_language] = $_result; - } - } - return $result; - } - if (is_string($language)) { - if (isset($this->_l10nCatalog[$language])) { - return $this->_l10nCatalog[$language]; - } - if (isset($this->_l10nMap[$language]) && isset($this->_l10nCatalog[$this->_l10nMap[$language]])) { - return $this->_l10nCatalog[$this->_l10nMap[$language]]; - } - return false; - } - return $this->_l10nCatalog; - } - } \ No newline at end of file diff --git a/lib/Cake/I18n/Multibyte.php b/lib/Cake/I18n/Multibyte.php index c11082ac..592e3dde 100755 --- a/lib/Cake/I18n/Multibyte.php +++ b/lib/Cake/I18n/Multibyte.php @@ -21,859 +21,879 @@ * * @package Cake.I18n */ -class Multibyte { - -/** - * Holds the case folding values - * - * @var array - */ - protected static $_caseFold = array(); - -/** - * Holds an array of Unicode code point ranges - * - * @var array - */ - protected static $_codeRange = array(); - -/** - * Holds the current code point range - * - * @var string - */ - protected static $_table = null; - -/** - * Converts a multibyte character string - * to the decimal value of the character - * - * @param string $string String to convert. - * @return array - */ - public static function utf8($string) { - $map = array(); - - $values = array(); - $find = 1; - $length = strlen($string); - - for ($i = 0; $i < $length; $i++) { - $value = ord($string[$i]); - - if ($value < 128) { - $map[] = $value; - } else { - if (empty($values)) { - $find = ($value < 224) ? 2 : 3; - } - $values[] = $value; - - if (count($values) === $find) { - if ($find == 3) { - $map[] = (($values[0] % 16) * 4096) + (($values[1] % 64) * 64) + ($values[2] % 64); - } else { - $map[] = (($values[0] % 32) * 64) + ($values[1] % 64); - } - $values = array(); - $find = 1; - } - } - } - return $map; - } - -/** - * Converts the decimal value of a multibyte character string - * to a string - * - * @param array $array Values array. - * @return string - */ - public static function ascii($array) { - $ascii = ''; - - foreach ($array as $utf8) { - if ($utf8 < 128) { - $ascii .= chr($utf8); - } elseif ($utf8 < 2048) { - $ascii .= chr(192 + (($utf8 - ($utf8 % 64)) / 64)); - $ascii .= chr(128 + ($utf8 % 64)); - } else { - $ascii .= chr(224 + (($utf8 - ($utf8 % 4096)) / 4096)); - $ascii .= chr(128 + ((($utf8 % 4096) - ($utf8 % 64)) / 64)); - $ascii .= chr(128 + ($utf8 % 64)); - } - } - return $ascii; - } - -/** - * Find position of first occurrence of a case-insensitive string. - * - * @param string $haystack The string from which to get the position of the first occurrence of $needle. - * @param string $needle The string to find in $haystack. - * @param int $offset The position in $haystack to start searching. - * @return int|bool The numeric position of the first occurrence of $needle in the $haystack string, - * or false if $needle is not found. - */ - public static function stripos($haystack, $needle, $offset = 0) { - if (Multibyte::checkMultibyte($haystack)) { - $haystack = Multibyte::strtoupper($haystack); - $needle = Multibyte::strtoupper($needle); - return Multibyte::strpos($haystack, $needle, $offset); - } - return stripos($haystack, $needle, $offset); - } - -/** - * Finds first occurrence of a string within another, case insensitive. - * - * @param string $haystack The string from which to get the first occurrence of $needle. - * @param string $needle The string to find in $haystack. - * @param bool $part Determines which portion of $haystack this function returns. - * If set to true, it returns all of $haystack from the beginning to the first occurrence of $needle. - * If set to false, it returns all of $haystack from the first occurrence of $needle to the end, - * Default value is false. - * @return int|bool The portion of $haystack, or false if $needle is not found. - */ - public static function stristr($haystack, $needle, $part = false) { - $php = (PHP_VERSION < 5.3); - - if (($php && $part) || Multibyte::checkMultibyte($haystack)) { - $check = Multibyte::strtoupper($haystack); - $check = Multibyte::utf8($check); - $found = false; - - $haystack = Multibyte::utf8($haystack); - $haystackCount = count($haystack); - - $needle = Multibyte::strtoupper($needle); - $needle = Multibyte::utf8($needle); - $needleCount = count($needle); - - $parts = array(); - $position = 0; - - while (($found === false) && ($position < $haystackCount)) { - if (isset($needle[0]) && $needle[0] === $check[$position]) { - for ($i = 1; $i < $needleCount; $i++) { - if ($needle[$i] !== $check[$position + $i]) { - break; - } - } - if ($i === $needleCount) { - $found = true; - } - } - if (!$found) { - $parts[] = $haystack[$position]; - unset($haystack[$position]); - } - $position++; - } - - if ($found && $part && !empty($parts)) { - return Multibyte::ascii($parts); - } elseif ($found && !empty($haystack)) { - return Multibyte::ascii($haystack); - } - return false; - } - - if (!$php) { - return stristr($haystack, $needle, $part); - } - return stristr($haystack, $needle); - } - -/** - * Get string length. - * - * @param string $string The string being checked for length. - * @return int The number of characters in string $string - */ - public static function strlen($string) { - if (Multibyte::checkMultibyte($string)) { - $string = Multibyte::utf8($string); - return count($string); - } - return strlen($string); - } - -/** - * Find position of first occurrence of a string. - * - * @param string $haystack The string being checked. - * @param string $needle The position counted from the beginning of haystack. - * @param int $offset The search offset. If it is not specified, 0 is used. - * @return int|bool The numeric position of the first occurrence of $needle in the $haystack string. - * If $needle is not found, it returns false. - */ - public static function strpos($haystack, $needle, $offset = 0) { - if (Multibyte::checkMultibyte($haystack)) { - $found = false; - - $haystack = Multibyte::utf8($haystack); - $haystackCount = count($haystack); - - $needle = Multibyte::utf8($needle); - $needleCount = count($needle); - - $position = $offset; - - while (($found === false) && ($position < $haystackCount)) { - if (isset($needle[0]) && $needle[0] === $haystack[$position]) { - for ($i = 1; $i < $needleCount; $i++) { - if ($needle[$i] !== $haystack[$position + $i]) { - break; - } - } - if ($i === $needleCount) { - $found = true; - $position--; - } - } - $position++; - } - if ($found) { - return $position; - } - return false; - } - return strpos($haystack, $needle, $offset); - } - -/** - * Finds the last occurrence of a character in a string within another. - * - * @param string $haystack The string from which to get the last occurrence of $needle. - * @param string $needle The string to find in $haystack. - * @param bool $part Determines which portion of $haystack this function returns. - * If set to true, it returns all of $haystack from the beginning to the last occurrence of $needle. - * If set to false, it returns all of $haystack from the last occurrence of $needle to the end, - * Default value is false. - * @return string|bool The portion of $haystack. or false if $needle is not found. - */ - public static function strrchr($haystack, $needle, $part = false) { - $check = Multibyte::utf8($haystack); - $found = false; - - $haystack = Multibyte::utf8($haystack); - $haystackCount = count($haystack); - - $matches = array_count_values($check); - - $needle = Multibyte::utf8($needle); - $needleCount = count($needle); - - $parts = array(); - $position = 0; - - while (($found === false) && ($position < $haystackCount)) { - if (isset($needle[0]) && $needle[0] === $check[$position]) { - for ($i = 1; $i < $needleCount; $i++) { - if ($needle[$i] !== $check[$position + $i]) { - if ($needle[$i] === $check[($position + $i) - 1]) { - $found = true; - } - unset($parts[$position - 1]); - $haystack = array_merge(array($haystack[$position]), $haystack); - break; - } - } - if (isset($matches[$needle[0]]) && $matches[$needle[0]] > 1) { - $matches[$needle[0]] = $matches[$needle[0]] - 1; - } elseif ($i === $needleCount) { - $found = true; - } - } - - if (!$found && isset($haystack[$position])) { - $parts[] = $haystack[$position]; - unset($haystack[$position]); - } - $position++; - } - - if ($found && $part && !empty($parts)) { - return Multibyte::ascii($parts); - } elseif ($found && !empty($haystack)) { - return Multibyte::ascii($haystack); - } - return false; - } - -/** - * Finds the last occurrence of a character in a string within another, case insensitive. - * - * @param string $haystack The string from which to get the last occurrence of $needle. - * @param string $needle The string to find in $haystack. - * @param bool $part Determines which portion of $haystack this function returns. - * If set to true, it returns all of $haystack from the beginning to the last occurrence of $needle. - * If set to false, it returns all of $haystack from the last occurrence of $needle to the end, - * Default value is false. - * @return string|bool The portion of $haystack. or false if $needle is not found. - */ - public static function strrichr($haystack, $needle, $part = false) { - $check = Multibyte::strtoupper($haystack); - $check = Multibyte::utf8($check); - $found = false; - - $haystack = Multibyte::utf8($haystack); - $haystackCount = count($haystack); - - $matches = array_count_values($check); - - $needle = Multibyte::strtoupper($needle); - $needle = Multibyte::utf8($needle); - $needleCount = count($needle); - - $parts = array(); - $position = 0; - - while (($found === false) && ($position < $haystackCount)) { - if (isset($needle[0]) && $needle[0] === $check[$position]) { - for ($i = 1; $i < $needleCount; $i++) { - if ($needle[$i] !== $check[$position + $i]) { - if ($needle[$i] === $check[($position + $i) - 1]) { - $found = true; - } - unset($parts[$position - 1]); - $haystack = array_merge(array($haystack[$position]), $haystack); - break; - } - } - if (isset($matches[$needle[0]]) && $matches[$needle[0]] > 1) { - $matches[$needle[0]] = $matches[$needle[0]] - 1; - } elseif ($i === $needleCount) { - $found = true; - } - } - - if (!$found && isset($haystack[$position])) { - $parts[] = $haystack[$position]; - unset($haystack[$position]); - } - $position++; - } - - if ($found && $part && !empty($parts)) { - return Multibyte::ascii($parts); - } elseif ($found && !empty($haystack)) { - return Multibyte::ascii($haystack); - } - return false; - } - -/** - * Finds position of last occurrence of a string within another, case insensitive - * - * @param string $haystack The string from which to get the position of the last occurrence of $needle. - * @param string $needle The string to find in $haystack. - * @param int $offset The position in $haystack to start searching. - * @return int|bool The numeric position of the last occurrence of $needle in the $haystack string, - * or false if $needle is not found. - */ - public static function strripos($haystack, $needle, $offset = 0) { - if (Multibyte::checkMultibyte($haystack)) { - $found = false; - $haystack = Multibyte::strtoupper($haystack); - $haystack = Multibyte::utf8($haystack); - $haystackCount = count($haystack); - - $matches = array_count_values($haystack); - - $needle = Multibyte::strtoupper($needle); - $needle = Multibyte::utf8($needle); - $needleCount = count($needle); - - $position = $offset; - - while (($found === false) && ($position < $haystackCount)) { - if (isset($needle[0]) && $needle[0] === $haystack[$position]) { - for ($i = 1; $i < $needleCount; $i++) { - if ($needle[$i] !== $haystack[$position + $i]) { - if ($needle[$i] === $haystack[($position + $i) - 1]) { - $position--; - $found = true; - continue; - } - } - } - - if (!$offset && isset($matches[$needle[0]]) && $matches[$needle[0]] > 1) { - $matches[$needle[0]] = $matches[$needle[0]] - 1; - } elseif ($i === $needleCount) { - $found = true; - $position--; - } - } - $position++; - } - return ($found) ? $position : false; - } - return strripos($haystack, $needle, $offset); - } - -/** - * Find position of last occurrence of a string in a string. - * - * @param string $haystack The string being checked, for the last occurrence of $needle. - * @param string $needle The string to find in $haystack. - * @param int $offset May be specified to begin searching an arbitrary number of characters into the string. - * Negative values will stop searching at an arbitrary point prior to the end of the string. - * @return int|bool The numeric position of the last occurrence of $needle in the $haystack string. - * If $needle is not found, it returns false. - */ - public static function strrpos($haystack, $needle, $offset = 0) { - if (Multibyte::checkMultibyte($haystack)) { - $found = false; - - $haystack = Multibyte::utf8($haystack); - $haystackCount = count($haystack); - - $matches = array_count_values($haystack); - - $needle = Multibyte::utf8($needle); - $needleCount = count($needle); - - $position = $offset; - - while (($found === false) && ($position < $haystackCount)) { - if (isset($needle[0]) && $needle[0] === $haystack[$position]) { - for ($i = 1; $i < $needleCount; $i++) { - if ($needle[$i] !== $haystack[$position + $i]) { - if ($needle[$i] === $haystack[($position + $i) - 1]) { - $position--; - $found = true; - continue; - } - } - } - - if (!$offset && isset($matches[$needle[0]]) && $matches[$needle[0]] > 1) { - $matches[$needle[0]] = $matches[$needle[0]] - 1; - } elseif ($i === $needleCount) { - $found = true; - $position--; - } - } - $position++; - } - return ($found) ? $position : false; - } - return strrpos($haystack, $needle, $offset); - } - -/** - * Finds first occurrence of a string within another - * - * @param string $haystack The string from which to get the first occurrence of $needle. - * @param string $needle The string to find in $haystack - * @param bool $part Determines which portion of $haystack this function returns. - * If set to true, it returns all of $haystack from the beginning to the first occurrence of $needle. - * If set to false, it returns all of $haystack from the first occurrence of $needle to the end, - * Default value is FALSE. - * @return string|bool The portion of $haystack, or true if $needle is not found. - */ - public static function strstr($haystack, $needle, $part = false) { - $php = (PHP_VERSION < 5.3); - - if (($php && $part) || Multibyte::checkMultibyte($haystack)) { - $check = Multibyte::utf8($haystack); - $found = false; - - $haystack = Multibyte::utf8($haystack); - $haystackCount = count($haystack); - - $needle = Multibyte::utf8($needle); - $needleCount = count($needle); - - $parts = array(); - $position = 0; - - while (($found === false) && ($position < $haystackCount)) { - if (isset($needle[0]) && $needle[0] === $check[$position]) { - for ($i = 1; $i < $needleCount; $i++) { - if ($needle[$i] !== $check[$position + $i]) { - break; - } - } - if ($i === $needleCount) { - $found = true; - } - } - if (!$found) { - $parts[] = $haystack[$position]; - unset($haystack[$position]); - } - $position++; - } - - if ($found && $part && !empty($parts)) { - return Multibyte::ascii($parts); - } elseif ($found && !empty($haystack)) { - return Multibyte::ascii($haystack); - } - return false; - } - - if (!$php) { - return strstr($haystack, $needle, $part); - } - return strstr($haystack, $needle); - } - -/** - * Make a string lowercase - * - * @param string $string The string being lowercased. - * @return string with all alphabetic characters converted to lowercase. - */ - public static function strtolower($string) { - $utf8Map = Multibyte::utf8($string); - - $length = count($utf8Map); - $lowerCase = array(); - - for ($i = 0; $i < $length; $i++) { - $char = $utf8Map[$i]; - - if ($char < 128) { - $str = strtolower(chr($char)); - $strlen = strlen($str); - for ($ii = 0; $ii < $strlen; $ii++) { - $lower = ord(substr($str, $ii, 1)); - } - $lowerCase[] = $lower; - $matched = true; - } else { - $matched = false; - $keys = static::_find($char, 'upper'); - - if (!empty($keys)) { - foreach ($keys as $key => $value) { - if ($keys[$key]['upper'] == $char && count($keys[$key]['lower']) > 0) { - $lowerCase[] = $keys[$key]['lower'][0]; - $matched = true; - break 1; - } - } - } - } - if ($matched === false) { - $lowerCase[] = $char; - } - } - return Multibyte::ascii($lowerCase); - } - -/** - * Make a string uppercase - * - * @param string $string The string being uppercased. - * @return string with all alphabetic characters converted to uppercase. - */ - public static function strtoupper($string) { - $utf8Map = Multibyte::utf8($string); - - $length = count($utf8Map); - $replaced = array(); - $upperCase = array(); - - for ($i = 0; $i < $length; $i++) { - $char = $utf8Map[$i]; - - if ($char < 128) { - $str = strtoupper(chr($char)); - $strlen = strlen($str); - for ($ii = 0; $ii < $strlen; $ii++) { - $upper = ord(substr($str, $ii, 1)); - } - $upperCase[] = $upper; - $matched = true; - - } else { - $matched = false; - $keys = static::_find($char); - $keyCount = count($keys); - - if (!empty($keys)) { - foreach ($keys as $key => $value) { - $matched = false; - $replace = 0; - if ($length > 1 && count($keys[$key]['lower']) > 1) { - $j = 0; - - for ($ii = 0, $count = count($keys[$key]['lower']); $ii < $count; $ii++) { - $nextChar = $utf8Map[$i + $ii]; - - if (isset($nextChar) && ($nextChar == $keys[$key]['lower'][$j + $ii])) { - $replace++; - } - } - if ($replace == $count) { - $upperCase[] = $keys[$key]['upper']; - $replaced = array_merge($replaced, array_values($keys[$key]['lower'])); - $matched = true; - break 1; - } - } elseif ($length > 1 && $keyCount > 1) { - $j = 0; - for ($ii = 1; $ii < $keyCount; $ii++) { - $nextChar = $utf8Map[$i + $ii - 1]; - - if (in_array($nextChar, $keys[$ii]['lower'])) { - - for ($jj = 0, $count = count($keys[$ii]['lower']); $jj < $count; $jj++) { - $nextChar = $utf8Map[$i + $jj]; - - if (isset($nextChar) && ($nextChar == $keys[$ii]['lower'][$j + $jj])) { - $replace++; - } - } - if ($replace == $count) { - $upperCase[] = $keys[$ii]['upper']; - $replaced = array_merge($replaced, array_values($keys[$ii]['lower'])); - $matched = true; - break 2; - } - } - } - } - if ($keys[$key]['lower'][0] == $char) { - $upperCase[] = $keys[$key]['upper']; - $matched = true; - break 1; - } - } - } - } - if ($matched === false && !in_array($char, $replaced, true)) { - $upperCase[] = $char; - } - } - return Multibyte::ascii($upperCase); - } - -/** - * Count the number of substring occurrences - * - * @param string $haystack The string being checked. - * @param string $needle The string being found. - * @return int The number of times the $needle substring occurs in the $haystack string. - */ - public static function substrCount($haystack, $needle) { - $count = 0; - $haystack = Multibyte::utf8($haystack); - $haystackCount = count($haystack); - $matches = array_count_values($haystack); - $needle = Multibyte::utf8($needle); - $needleCount = count($needle); - - if ($needleCount === 1 && isset($matches[$needle[0]])) { - return $matches[$needle[0]]; - } - - for ($i = 0; $i < $haystackCount; $i++) { - if (isset($needle[0]) && $needle[0] === $haystack[$i]) { - for ($ii = 1; $ii < $needleCount; $ii++) { - if ($needle[$ii] === $haystack[$i + 1]) { - if ((isset($needle[$ii + 1]) && $haystack[$i + 2]) && $needle[$ii + 1] !== $haystack[$i + 2]) { - $count--; - } else { - $count++; - } - } - } - } - } - return $count; - } - -/** - * Get part of string - * - * @param string $string The string being checked. - * @param int $start The first position used in $string. - * @param int $length The maximum length of the returned string. - * @return string The portion of $string specified by the $string and $length parameters. - */ - public static function substr($string, $start, $length = null) { - if ($start === 0 && $length === null) { - return $string; - } - - $string = Multibyte::utf8($string); - - for ($i = 1; $i <= $start; $i++) { - unset($string[$i - 1]); - } - - if ($length === null || count($string) < $length) { - return Multibyte::ascii($string); - } - $string = array_values($string); - - $value = array(); - for ($i = 0; $i < $length; $i++) { - $value[] = $string[$i]; - } - return Multibyte::ascii($value); - } - -/** - * Prepare a string for mail transport, using the provided encoding - * - * @param string $string value to encode - * @param string $charset charset to use for encoding. defaults to UTF-8 - * @param string $newline Newline string. - * @return string - */ - public static function mimeEncode($string, $charset = null, $newline = "\r\n") { - if (!Multibyte::checkMultibyte($string) && strlen($string) < 75) { - return $string; - } - - if (empty($charset)) { - $charset = Configure::read('App.encoding'); - } - $charset = strtoupper($charset); - - $start = '=?' . $charset . '?B?'; - $end = '?='; - $spacer = $end . $newline . ' ' . $start; - - $length = 75 - strlen($start) - strlen($end); - $length = $length - ($length % 4); - if ($charset === 'UTF-8') { - $parts = array(); - $maxchars = floor(($length * 3) / 4); - $stringLength = strlen($string); - while ($stringLength > $maxchars) { - $i = (int)$maxchars; - $test = ord($string[$i]); - while ($test >= 128 && $test <= 191) { - $i--; - $test = ord($string[$i]); - } - $parts[] = base64_encode(substr($string, 0, $i)); - $string = substr($string, $i); - $stringLength = strlen($string); - } - $parts[] = base64_encode($string); - $string = implode($spacer, $parts); - } else { - $string = chunk_split(base64_encode($string), $length, $spacer); - $string = preg_replace('/' . preg_quote($spacer) . '$/', '', $string); - } - return $start . $string . $end; - } - -/** - * Return the Code points range for Unicode characters - * - * @param int $decimal Decimal value. - * @return string - */ - protected static function _codepoint($decimal) { - if ($decimal > 128 && $decimal < 256) { - $return = '0080_00ff'; // Latin-1 Supplement - } elseif ($decimal < 384) { - $return = '0100_017f'; // Latin Extended-A - } elseif ($decimal < 592) { - $return = '0180_024F'; // Latin Extended-B - } elseif ($decimal < 688) { - $return = '0250_02af'; // IPA Extensions - } elseif ($decimal >= 880 && $decimal < 1024) { - $return = '0370_03ff'; // Greek and Coptic - } elseif ($decimal < 1280) { - $return = '0400_04ff'; // Cyrillic - } elseif ($decimal < 1328) { - $return = '0500_052f'; // Cyrillic Supplement - } elseif ($decimal < 1424) { - $return = '0530_058f'; // Armenian - } elseif ($decimal >= 7680 && $decimal < 7936) { - $return = '1e00_1eff'; // Latin Extended Additional - } elseif ($decimal < 8192) { - $return = '1f00_1fff'; // Greek Extended - } elseif ($decimal >= 8448 && $decimal < 8528) { - $return = '2100_214f'; // Letterlike Symbols - } elseif ($decimal < 8592) { - $return = '2150_218f'; // Number Forms - } elseif ($decimal >= 9312 && $decimal < 9472) { - $return = '2460_24ff'; // Enclosed Alphanumerics - } elseif ($decimal >= 11264 && $decimal < 11360) { - $return = '2c00_2c5f'; // Glagolitic - } elseif ($decimal < 11392) { - $return = '2c60_2c7f'; // Latin Extended-C - } elseif ($decimal < 11520) { - $return = '2c80_2cff'; // Coptic - } elseif ($decimal >= 65280 && $decimal < 65520) { - $return = 'ff00_ffef'; // Halfwidth and Fullwidth Forms - } else { - $return = false; - } - static::$_codeRange[$decimal] = $return; - return $return; - } - -/** - * Find the related code folding values for $char - * - * @param int $char decimal value of character - * @param string $type Type 'lower' or 'upper'. Defaults to 'lower'. - * @return array - */ - protected static function _find($char, $type = 'lower') { - $found = array(); - if (!isset(static::$_codeRange[$char])) { - $range = static::_codepoint($char); - if ($range === false) { - return array(); - } - if (!Configure::configured('_cake_core_')) { - App::uses('PhpReader', 'Configure'); - Configure::config('_cake_core_', new PhpReader(CAKE . 'Config' . DS)); - } - Configure::load('unicode' . DS . 'casefolding' . DS . $range, '_cake_core_'); - static::$_caseFold[$range] = Configure::read($range); - Configure::delete($range); - } - - if (!static::$_codeRange[$char]) { - return array(); - } - static::$_table = static::$_codeRange[$char]; - $count = count(static::$_caseFold[static::$_table]); - - for ($i = 0; $i < $count; $i++) { - if ($type === 'lower' && static::$_caseFold[static::$_table][$i][$type][0] === $char) { - $found[] = static::$_caseFold[static::$_table][$i]; - } elseif ($type === 'upper' && static::$_caseFold[static::$_table][$i][$type] === $char) { - $found[] = static::$_caseFold[static::$_table][$i]; - } - } - return $found; - } - -/** - * Check the $string for multibyte characters - * - * @param string $string Value to test. - * @return bool - */ - public static function checkMultibyte($string) { - $length = strlen($string); - - for ($i = 0; $i < $length; $i++) { - $value = ord(($string[$i])); - if ($value > 128) { - return true; - } - } - return false; - } +class Multibyte +{ + + /** + * Holds the case folding values + * + * @var array + */ + protected static $_caseFold = []; + + /** + * Holds an array of Unicode code point ranges + * + * @var array + */ + protected static $_codeRange = []; + + /** + * Holds the current code point range + * + * @var string + */ + protected static $_table = null; + + /** + * Find position of first occurrence of a case-insensitive string. + * + * @param string $haystack The string from which to get the position of the first occurrence of $needle. + * @param string $needle The string to find in $haystack. + * @param int $offset The position in $haystack to start searching. + * @return int|bool The numeric position of the first occurrence of $needle in the $haystack string, + * or false if $needle is not found. + */ + public static function stripos($haystack, $needle, $offset = 0) + { + if (Multibyte::checkMultibyte($haystack)) { + $haystack = Multibyte::strtoupper($haystack); + $needle = Multibyte::strtoupper($needle); + return Multibyte::strpos($haystack, $needle, $offset); + } + return stripos($haystack, $needle, $offset); + } + + /** + * Check the $string for multibyte characters + * + * @param string $string Value to test. + * @return bool + */ + public static function checkMultibyte($string) + { + $length = strlen($string); + + for ($i = 0; $i < $length; $i++) { + $value = ord(($string[$i])); + if ($value > 128) { + return true; + } + } + return false; + } + + /** + * Make a string uppercase + * + * @param string $string The string being uppercased. + * @return string with all alphabetic characters converted to uppercase. + */ + public static function strtoupper($string) + { + $utf8Map = Multibyte::utf8($string); + + $length = count($utf8Map); + $replaced = []; + $upperCase = []; + + for ($i = 0; $i < $length; $i++) { + $char = $utf8Map[$i]; + + if ($char < 128) { + $str = strtoupper(chr($char)); + $strlen = strlen($str); + for ($ii = 0; $ii < $strlen; $ii++) { + $upper = ord(substr($str, $ii, 1)); + } + $upperCase[] = $upper; + $matched = true; + + } else { + $matched = false; + $keys = static::_find($char); + $keyCount = count($keys); + + if (!empty($keys)) { + foreach ($keys as $key => $value) { + $matched = false; + $replace = 0; + if ($length > 1 && count($keys[$key]['lower']) > 1) { + $j = 0; + + for ($ii = 0, $count = count($keys[$key]['lower']); $ii < $count; $ii++) { + $nextChar = $utf8Map[$i + $ii]; + + if (isset($nextChar) && ($nextChar == $keys[$key]['lower'][$j + $ii])) { + $replace++; + } + } + if ($replace == $count) { + $upperCase[] = $keys[$key]['upper']; + $replaced = array_merge($replaced, array_values($keys[$key]['lower'])); + $matched = true; + break 1; + } + } else if ($length > 1 && $keyCount > 1) { + $j = 0; + for ($ii = 1; $ii < $keyCount; $ii++) { + $nextChar = $utf8Map[$i + $ii - 1]; + + if (in_array($nextChar, $keys[$ii]['lower'])) { + + for ($jj = 0, $count = count($keys[$ii]['lower']); $jj < $count; $jj++) { + $nextChar = $utf8Map[$i + $jj]; + + if (isset($nextChar) && ($nextChar == $keys[$ii]['lower'][$j + $jj])) { + $replace++; + } + } + if ($replace == $count) { + $upperCase[] = $keys[$ii]['upper']; + $replaced = array_merge($replaced, array_values($keys[$ii]['lower'])); + $matched = true; + break 2; + } + } + } + } + if ($keys[$key]['lower'][0] == $char) { + $upperCase[] = $keys[$key]['upper']; + $matched = true; + break 1; + } + } + } + } + if ($matched === false && !in_array($char, $replaced, true)) { + $upperCase[] = $char; + } + } + return Multibyte::ascii($upperCase); + } + + /** + * Converts a multibyte character string + * to the decimal value of the character + * + * @param string $string String to convert. + * @return array + */ + public static function utf8($string) + { + $map = []; + + $values = []; + $find = 1; + $length = strlen($string); + + for ($i = 0; $i < $length; $i++) { + $value = ord($string[$i]); + + if ($value < 128) { + $map[] = $value; + } else { + if (empty($values)) { + $find = ($value < 224) ? 2 : 3; + } + $values[] = $value; + + if (count($values) === $find) { + if ($find == 3) { + $map[] = (($values[0] % 16) * 4096) + (($values[1] % 64) * 64) + ($values[2] % 64); + } else { + $map[] = (($values[0] % 32) * 64) + ($values[1] % 64); + } + $values = []; + $find = 1; + } + } + } + return $map; + } + + /** + * Find the related code folding values for $char + * + * @param int $char decimal value of character + * @param string $type Type 'lower' or 'upper'. Defaults to 'lower'. + * @return array + */ + protected static function _find($char, $type = 'lower') + { + $found = []; + if (!isset(static::$_codeRange[$char])) { + $range = static::_codepoint($char); + if ($range === false) { + return []; + } + if (!Configure::configured('_cake_core_')) { + App::uses('PhpReader', 'Configure'); + Configure::config('_cake_core_', new PhpReader(CAKE . 'Config' . DS)); + } + Configure::load('unicode' . DS . 'casefolding' . DS . $range, '_cake_core_'); + static::$_caseFold[$range] = Configure::read($range); + Configure::delete($range); + } + + if (!static::$_codeRange[$char]) { + return []; + } + static::$_table = static::$_codeRange[$char]; + $count = count(static::$_caseFold[static::$_table]); + + for ($i = 0; $i < $count; $i++) { + if ($type === 'lower' && static::$_caseFold[static::$_table][$i][$type][0] === $char) { + $found[] = static::$_caseFold[static::$_table][$i]; + } else if ($type === 'upper' && static::$_caseFold[static::$_table][$i][$type] === $char) { + $found[] = static::$_caseFold[static::$_table][$i]; + } + } + return $found; + } + + /** + * Return the Code points range for Unicode characters + * + * @param int $decimal Decimal value. + * @return string + */ + protected static function _codepoint($decimal) + { + if ($decimal > 128 && $decimal < 256) { + $return = '0080_00ff'; // Latin-1 Supplement + } else if ($decimal < 384) { + $return = '0100_017f'; // Latin Extended-A + } else if ($decimal < 592) { + $return = '0180_024F'; // Latin Extended-B + } else if ($decimal < 688) { + $return = '0250_02af'; // IPA Extensions + } else if ($decimal >= 880 && $decimal < 1024) { + $return = '0370_03ff'; // Greek and Coptic + } else if ($decimal < 1280) { + $return = '0400_04ff'; // Cyrillic + } else if ($decimal < 1328) { + $return = '0500_052f'; // Cyrillic Supplement + } else if ($decimal < 1424) { + $return = '0530_058f'; // Armenian + } else if ($decimal >= 7680 && $decimal < 7936) { + $return = '1e00_1eff'; // Latin Extended Additional + } else if ($decimal < 8192) { + $return = '1f00_1fff'; // Greek Extended + } else if ($decimal >= 8448 && $decimal < 8528) { + $return = '2100_214f'; // Letterlike Symbols + } else if ($decimal < 8592) { + $return = '2150_218f'; // Number Forms + } else if ($decimal >= 9312 && $decimal < 9472) { + $return = '2460_24ff'; // Enclosed Alphanumerics + } else if ($decimal >= 11264 && $decimal < 11360) { + $return = '2c00_2c5f'; // Glagolitic + } else if ($decimal < 11392) { + $return = '2c60_2c7f'; // Latin Extended-C + } else if ($decimal < 11520) { + $return = '2c80_2cff'; // Coptic + } else if ($decimal >= 65280 && $decimal < 65520) { + $return = 'ff00_ffef'; // Halfwidth and Fullwidth Forms + } else { + $return = false; + } + static::$_codeRange[$decimal] = $return; + return $return; + } + + /** + * Converts the decimal value of a multibyte character string + * to a string + * + * @param array $array Values array. + * @return string + */ + public static function ascii($array) + { + $ascii = ''; + + foreach ($array as $utf8) { + if ($utf8 < 128) { + $ascii .= chr($utf8); + } else if ($utf8 < 2048) { + $ascii .= chr(192 + (($utf8 - ($utf8 % 64)) / 64)); + $ascii .= chr(128 + ($utf8 % 64)); + } else { + $ascii .= chr(224 + (($utf8 - ($utf8 % 4096)) / 4096)); + $ascii .= chr(128 + ((($utf8 % 4096) - ($utf8 % 64)) / 64)); + $ascii .= chr(128 + ($utf8 % 64)); + } + } + return $ascii; + } + + /** + * Find position of first occurrence of a string. + * + * @param string $haystack The string being checked. + * @param string $needle The position counted from the beginning of haystack. + * @param int $offset The search offset. If it is not specified, 0 is used. + * @return int|bool The numeric position of the first occurrence of $needle in the $haystack string. + * If $needle is not found, it returns false. + */ + public static function strpos($haystack, $needle, $offset = 0) + { + if (Multibyte::checkMultibyte($haystack)) { + $found = false; + + $haystack = Multibyte::utf8($haystack); + $haystackCount = count($haystack); + + $needle = Multibyte::utf8($needle); + $needleCount = count($needle); + + $position = $offset; + + while (($found === false) && ($position < $haystackCount)) { + if (isset($needle[0]) && $needle[0] === $haystack[$position]) { + for ($i = 1; $i < $needleCount; $i++) { + if ($needle[$i] !== $haystack[$position + $i]) { + break; + } + } + if ($i === $needleCount) { + $found = true; + $position--; + } + } + $position++; + } + if ($found) { + return $position; + } + return false; + } + return strpos($haystack, $needle, $offset); + } + + /** + * Finds first occurrence of a string within another, case insensitive. + * + * @param string $haystack The string from which to get the first occurrence of $needle. + * @param string $needle The string to find in $haystack. + * @param bool $part Determines which portion of $haystack this function returns. + * If set to true, it returns all of $haystack from the beginning to the first occurrence of $needle. + * If set to false, it returns all of $haystack from the first occurrence of $needle to the end, + * Default value is false. + * @return int|bool The portion of $haystack, or false if $needle is not found. + */ + public static function stristr($haystack, $needle, $part = false) + { + $php = (PHP_VERSION < 5.3); + + if (($php && $part) || Multibyte::checkMultibyte($haystack)) { + $check = Multibyte::strtoupper($haystack); + $check = Multibyte::utf8($check); + $found = false; + + $haystack = Multibyte::utf8($haystack); + $haystackCount = count($haystack); + + $needle = Multibyte::strtoupper($needle); + $needle = Multibyte::utf8($needle); + $needleCount = count($needle); + + $parts = []; + $position = 0; + + while (($found === false) && ($position < $haystackCount)) { + if (isset($needle[0]) && $needle[0] === $check[$position]) { + for ($i = 1; $i < $needleCount; $i++) { + if ($needle[$i] !== $check[$position + $i]) { + break; + } + } + if ($i === $needleCount) { + $found = true; + } + } + if (!$found) { + $parts[] = $haystack[$position]; + unset($haystack[$position]); + } + $position++; + } + + if ($found && $part && !empty($parts)) { + return Multibyte::ascii($parts); + } else if ($found && !empty($haystack)) { + return Multibyte::ascii($haystack); + } + return false; + } + + if (!$php) { + return stristr($haystack, $needle, $part); + } + return stristr($haystack, $needle); + } + + /** + * Get string length. + * + * @param string $string The string being checked for length. + * @return int The number of characters in string $string + */ + public static function strlen($string) + { + if (Multibyte::checkMultibyte($string)) { + $string = Multibyte::utf8($string); + return count($string); + } + return strlen($string); + } + + /** + * Finds the last occurrence of a character in a string within another. + * + * @param string $haystack The string from which to get the last occurrence of $needle. + * @param string $needle The string to find in $haystack. + * @param bool $part Determines which portion of $haystack this function returns. + * If set to true, it returns all of $haystack from the beginning to the last occurrence of $needle. + * If set to false, it returns all of $haystack from the last occurrence of $needle to the end, + * Default value is false. + * @return string|bool The portion of $haystack. or false if $needle is not found. + */ + public static function strrchr($haystack, $needle, $part = false) + { + $check = Multibyte::utf8($haystack); + $found = false; + + $haystack = Multibyte::utf8($haystack); + $haystackCount = count($haystack); + + $matches = array_count_values($check); + + $needle = Multibyte::utf8($needle); + $needleCount = count($needle); + + $parts = []; + $position = 0; + + while (($found === false) && ($position < $haystackCount)) { + if (isset($needle[0]) && $needle[0] === $check[$position]) { + for ($i = 1; $i < $needleCount; $i++) { + if ($needle[$i] !== $check[$position + $i]) { + if ($needle[$i] === $check[($position + $i) - 1]) { + $found = true; + } + unset($parts[$position - 1]); + $haystack = array_merge([$haystack[$position]], $haystack); + break; + } + } + if (isset($matches[$needle[0]]) && $matches[$needle[0]] > 1) { + $matches[$needle[0]] = $matches[$needle[0]] - 1; + } else if ($i === $needleCount) { + $found = true; + } + } + + if (!$found && isset($haystack[$position])) { + $parts[] = $haystack[$position]; + unset($haystack[$position]); + } + $position++; + } + + if ($found && $part && !empty($parts)) { + return Multibyte::ascii($parts); + } else if ($found && !empty($haystack)) { + return Multibyte::ascii($haystack); + } + return false; + } + + /** + * Finds the last occurrence of a character in a string within another, case insensitive. + * + * @param string $haystack The string from which to get the last occurrence of $needle. + * @param string $needle The string to find in $haystack. + * @param bool $part Determines which portion of $haystack this function returns. + * If set to true, it returns all of $haystack from the beginning to the last occurrence of $needle. + * If set to false, it returns all of $haystack from the last occurrence of $needle to the end, + * Default value is false. + * @return string|bool The portion of $haystack. or false if $needle is not found. + */ + public static function strrichr($haystack, $needle, $part = false) + { + $check = Multibyte::strtoupper($haystack); + $check = Multibyte::utf8($check); + $found = false; + + $haystack = Multibyte::utf8($haystack); + $haystackCount = count($haystack); + + $matches = array_count_values($check); + + $needle = Multibyte::strtoupper($needle); + $needle = Multibyte::utf8($needle); + $needleCount = count($needle); + + $parts = []; + $position = 0; + + while (($found === false) && ($position < $haystackCount)) { + if (isset($needle[0]) && $needle[0] === $check[$position]) { + for ($i = 1; $i < $needleCount; $i++) { + if ($needle[$i] !== $check[$position + $i]) { + if ($needle[$i] === $check[($position + $i) - 1]) { + $found = true; + } + unset($parts[$position - 1]); + $haystack = array_merge([$haystack[$position]], $haystack); + break; + } + } + if (isset($matches[$needle[0]]) && $matches[$needle[0]] > 1) { + $matches[$needle[0]] = $matches[$needle[0]] - 1; + } else if ($i === $needleCount) { + $found = true; + } + } + + if (!$found && isset($haystack[$position])) { + $parts[] = $haystack[$position]; + unset($haystack[$position]); + } + $position++; + } + + if ($found && $part && !empty($parts)) { + return Multibyte::ascii($parts); + } else if ($found && !empty($haystack)) { + return Multibyte::ascii($haystack); + } + return false; + } + + /** + * Finds position of last occurrence of a string within another, case insensitive + * + * @param string $haystack The string from which to get the position of the last occurrence of $needle. + * @param string $needle The string to find in $haystack. + * @param int $offset The position in $haystack to start searching. + * @return int|bool The numeric position of the last occurrence of $needle in the $haystack string, + * or false if $needle is not found. + */ + public static function strripos($haystack, $needle, $offset = 0) + { + if (Multibyte::checkMultibyte($haystack)) { + $found = false; + $haystack = Multibyte::strtoupper($haystack); + $haystack = Multibyte::utf8($haystack); + $haystackCount = count($haystack); + + $matches = array_count_values($haystack); + + $needle = Multibyte::strtoupper($needle); + $needle = Multibyte::utf8($needle); + $needleCount = count($needle); + + $position = $offset; + + while (($found === false) && ($position < $haystackCount)) { + if (isset($needle[0]) && $needle[0] === $haystack[$position]) { + for ($i = 1; $i < $needleCount; $i++) { + if ($needle[$i] !== $haystack[$position + $i]) { + if ($needle[$i] === $haystack[($position + $i) - 1]) { + $position--; + $found = true; + continue; + } + } + } + + if (!$offset && isset($matches[$needle[0]]) && $matches[$needle[0]] > 1) { + $matches[$needle[0]] = $matches[$needle[0]] - 1; + } else if ($i === $needleCount) { + $found = true; + $position--; + } + } + $position++; + } + return ($found) ? $position : false; + } + return strripos($haystack, $needle, $offset); + } + + /** + * Find position of last occurrence of a string in a string. + * + * @param string $haystack The string being checked, for the last occurrence of $needle. + * @param string $needle The string to find in $haystack. + * @param int $offset May be specified to begin searching an arbitrary number of characters into the string. + * Negative values will stop searching at an arbitrary point prior to the end of the string. + * @return int|bool The numeric position of the last occurrence of $needle in the $haystack string. + * If $needle is not found, it returns false. + */ + public static function strrpos($haystack, $needle, $offset = 0) + { + if (Multibyte::checkMultibyte($haystack)) { + $found = false; + + $haystack = Multibyte::utf8($haystack); + $haystackCount = count($haystack); + + $matches = array_count_values($haystack); + + $needle = Multibyte::utf8($needle); + $needleCount = count($needle); + + $position = $offset; + + while (($found === false) && ($position < $haystackCount)) { + if (isset($needle[0]) && $needle[0] === $haystack[$position]) { + for ($i = 1; $i < $needleCount; $i++) { + if ($needle[$i] !== $haystack[$position + $i]) { + if ($needle[$i] === $haystack[($position + $i) - 1]) { + $position--; + $found = true; + continue; + } + } + } + + if (!$offset && isset($matches[$needle[0]]) && $matches[$needle[0]] > 1) { + $matches[$needle[0]] = $matches[$needle[0]] - 1; + } else if ($i === $needleCount) { + $found = true; + $position--; + } + } + $position++; + } + return ($found) ? $position : false; + } + return strrpos($haystack, $needle, $offset); + } + + /** + * Finds first occurrence of a string within another + * + * @param string $haystack The string from which to get the first occurrence of $needle. + * @param string $needle The string to find in $haystack + * @param bool $part Determines which portion of $haystack this function returns. + * If set to true, it returns all of $haystack from the beginning to the first occurrence of $needle. + * If set to false, it returns all of $haystack from the first occurrence of $needle to the end, + * Default value is FALSE. + * @return string|bool The portion of $haystack, or true if $needle is not found. + */ + public static function strstr($haystack, $needle, $part = false) + { + $php = (PHP_VERSION < 5.3); + + if (($php && $part) || Multibyte::checkMultibyte($haystack)) { + $check = Multibyte::utf8($haystack); + $found = false; + + $haystack = Multibyte::utf8($haystack); + $haystackCount = count($haystack); + + $needle = Multibyte::utf8($needle); + $needleCount = count($needle); + + $parts = []; + $position = 0; + + while (($found === false) && ($position < $haystackCount)) { + if (isset($needle[0]) && $needle[0] === $check[$position]) { + for ($i = 1; $i < $needleCount; $i++) { + if ($needle[$i] !== $check[$position + $i]) { + break; + } + } + if ($i === $needleCount) { + $found = true; + } + } + if (!$found) { + $parts[] = $haystack[$position]; + unset($haystack[$position]); + } + $position++; + } + + if ($found && $part && !empty($parts)) { + return Multibyte::ascii($parts); + } else if ($found && !empty($haystack)) { + return Multibyte::ascii($haystack); + } + return false; + } + + if (!$php) { + return strstr($haystack, $needle, $part); + } + return strstr($haystack, $needle); + } + + /** + * Make a string lowercase + * + * @param string $string The string being lowercased. + * @return string with all alphabetic characters converted to lowercase. + */ + public static function strtolower($string) + { + $utf8Map = Multibyte::utf8($string); + + $length = count($utf8Map); + $lowerCase = []; + + for ($i = 0; $i < $length; $i++) { + $char = $utf8Map[$i]; + + if ($char < 128) { + $str = strtolower(chr($char)); + $strlen = strlen($str); + for ($ii = 0; $ii < $strlen; $ii++) { + $lower = ord(substr($str, $ii, 1)); + } + $lowerCase[] = $lower; + $matched = true; + } else { + $matched = false; + $keys = static::_find($char, 'upper'); + + if (!empty($keys)) { + foreach ($keys as $key => $value) { + if ($keys[$key]['upper'] == $char && count($keys[$key]['lower']) > 0) { + $lowerCase[] = $keys[$key]['lower'][0]; + $matched = true; + break 1; + } + } + } + } + if ($matched === false) { + $lowerCase[] = $char; + } + } + return Multibyte::ascii($lowerCase); + } + + /** + * Count the number of substring occurrences + * + * @param string $haystack The string being checked. + * @param string $needle The string being found. + * @return int The number of times the $needle substring occurs in the $haystack string. + */ + public static function substrCount($haystack, $needle) + { + $count = 0; + $haystack = Multibyte::utf8($haystack); + $haystackCount = count($haystack); + $matches = array_count_values($haystack); + $needle = Multibyte::utf8($needle); + $needleCount = count($needle); + + if ($needleCount === 1 && isset($matches[$needle[0]])) { + return $matches[$needle[0]]; + } + + for ($i = 0; $i < $haystackCount; $i++) { + if (isset($needle[0]) && $needle[0] === $haystack[$i]) { + for ($ii = 1; $ii < $needleCount; $ii++) { + if ($needle[$ii] === $haystack[$i + 1]) { + if ((isset($needle[$ii + 1]) && $haystack[$i + 2]) && $needle[$ii + 1] !== $haystack[$i + 2]) { + $count--; + } else { + $count++; + } + } + } + } + } + return $count; + } + + /** + * Get part of string + * + * @param string $string The string being checked. + * @param int $start The first position used in $string. + * @param int $length The maximum length of the returned string. + * @return string The portion of $string specified by the $string and $length parameters. + */ + public static function substr($string, $start, $length = null) + { + if ($start === 0 && $length === null) { + return $string; + } + + $string = Multibyte::utf8($string); + + for ($i = 1; $i <= $start; $i++) { + unset($string[$i - 1]); + } + + if ($length === null || count($string) < $length) { + return Multibyte::ascii($string); + } + $string = array_values($string); + + $value = []; + for ($i = 0; $i < $length; $i++) { + $value[] = $string[$i]; + } + return Multibyte::ascii($value); + } + + /** + * Prepare a string for mail transport, using the provided encoding + * + * @param string $string value to encode + * @param string $charset charset to use for encoding. defaults to UTF-8 + * @param string $newline Newline string. + * @return string + */ + public static function mimeEncode($string, $charset = null, $newline = "\r\n") + { + if (!Multibyte::checkMultibyte($string) && strlen($string) < 75) { + return $string; + } + + if (empty($charset)) { + $charset = Configure::read('App.encoding'); + } + $charset = strtoupper($charset); + + $start = '=?' . $charset . '?B?'; + $end = '?='; + $spacer = $end . $newline . ' ' . $start; + + $length = 75 - strlen($start) - strlen($end); + $length = $length - ($length % 4); + if ($charset === 'UTF-8') { + $parts = []; + $maxchars = floor(($length * 3) / 4); + $stringLength = strlen($string); + while ($stringLength > $maxchars) { + $i = (int)$maxchars; + $test = ord($string[$i]); + while ($test >= 128 && $test <= 191) { + $i--; + $test = ord($string[$i]); + } + $parts[] = base64_encode(substr($string, 0, $i)); + $string = substr($string, $i); + $stringLength = strlen($string); + } + $parts[] = base64_encode($string); + $string = implode($spacer, $parts); + } else { + $string = chunk_split(base64_encode($string), $length, $spacer); + $string = preg_replace('/' . preg_quote($spacer) . '$/', '', $string); + } + return $start . $string . $end; + } } diff --git a/lib/Cake/Log/CakeLog.php b/lib/Cake/Log/CakeLog.php index 3b2cfb6c..f83934e6 100755 --- a/lib/Cake/Log/CakeLog.php +++ b/lib/Cake/Log/CakeLog.php @@ -72,477 +72,497 @@ * @package Cake.Log * @link https://book.cakephp.org/2.0/en/core-libraries/logging.html#logging */ -class CakeLog { +class CakeLog +{ -/** - * LogEngineCollection class - * - * @var LogEngineCollection - */ - protected static $_Collection; + /** + * LogEngineCollection class + * + * @var LogEngineCollection + */ + protected static $_Collection; -/** - * Default log levels as detailed in RFC 5424 - * http://tools.ietf.org/html/rfc5424 - * - * Windows has fewer levels, thus notice, info and debug are the same. - * https://bugs.php.net/bug.php?id=18090 - * - * @var array - */ - protected static $_defaultLevels = array( - 'emergency' => LOG_EMERG, - 'alert' => LOG_ALERT, - 'critical' => LOG_CRIT, - 'error' => LOG_ERR, - 'warning' => LOG_WARNING, - 'notice' => LOG_NOTICE, - 'info' => LOG_INFO, - 'debug' => LOG_DEBUG, - ); + /** + * Default log levels as detailed in RFC 5424 + * http://tools.ietf.org/html/rfc5424 + * + * Windows has fewer levels, thus notice, info and debug are the same. + * https://bugs.php.net/bug.php?id=18090 + * + * @var array + */ + protected static $_defaultLevels = [ + 'emergency' => LOG_EMERG, + 'alert' => LOG_ALERT, + 'critical' => LOG_CRIT, + 'error' => LOG_ERR, + 'warning' => LOG_WARNING, + 'notice' => LOG_NOTICE, + 'info' => LOG_INFO, + 'debug' => LOG_DEBUG, + ]; -/** - * Active log levels for this instance. - * - * @var array - */ - protected static $_levels; + /** + * Active log levels for this instance. + * + * @var array + */ + protected static $_levels; -/** - * Mapped log levels - * - * @var array - */ - protected static $_levelMap; + /** + * Mapped log levels + * + * @var array + */ + protected static $_levelMap; -/** - * initialize ObjectCollection - * - * @return void - */ - protected static function _init() { - static::$_levels = static::defaultLevels(); - static::$_Collection = new LogEngineCollection(); - } + /** + * Configure and add a new logging stream to CakeLog + * You can use add loggers from app/Log/Engine use app.loggername, or any + * plugin/Log/Engine using plugin.loggername. + * + * ### Usage: + * + * ``` + * CakeLog::config('second_file', array( + * 'engine' => 'File', + * 'path' => '/var/logs/my_app/' + * )); + * ``` + * + * Will configure a FileLog instance to use the specified path. + * All options that are not `engine` are passed onto the logging adapter, + * and handled there. Any class can be configured as a logging + * adapter as long as it implements the methods in CakeLogInterface. + * + * ### Logging levels + * + * When configuring loggers, you can set which levels a logger will handle. + * This allows you to disable debug messages in production for example: + * + * ``` + * CakeLog::config('default', array( + * 'engine' => 'File', + * 'path' => LOGS, + * 'levels' => array('error', 'critical', 'alert', 'emergency') + * )); + * ``` + * + * The above logger would only log error messages or higher. Any + * other log messages would be discarded. + * + * ### Logging scopes + * + * When configuring loggers you can define the active scopes the logger + * is for. If defined only the listed scopes will be handled by the + * logger. If you don't define any scopes an adapter will catch + * all scopes that match the handled levels. + * + * ``` + * CakeLog::config('payments', array( + * 'engine' => 'File', + * 'types' => array('info', 'error', 'warning'), + * 'scopes' => array('payment', 'order') + * )); + * ``` + * + * The above logger will only capture log entries made in the + * `payment` and `order` scopes. All other scopes including the + * undefined scope will be ignored. Its important to remember that + * when using scopes you must also define the `types` of log messages + * that a logger will handle. Failing to do so will result in the logger + * catching all log messages even if the scope is incorrect. + * + * @param string $key The keyname for this logger, used to remove the + * logger later. + * @param array $config Array of configuration information for the logger + * @return bool success of configuration. + * @throws CakeLogException + * @link https://book.cakephp.org/2.0/en/core-libraries/logging.html#creating-and-configuring-log-streams + */ + public static function config($key, $config) + { + if (!preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/', $key)) { + throw new CakeLogException(__d('cake_dev', 'Invalid key name')); + } + if (empty($config['engine'])) { + throw new CakeLogException(__d('cake_dev', 'Missing logger class name')); + } + if (empty(static::$_Collection)) { + static::_init(); + } + static::$_Collection->load($key, $config); + return true; + } -/** - * Configure and add a new logging stream to CakeLog - * You can use add loggers from app/Log/Engine use app.loggername, or any - * plugin/Log/Engine using plugin.loggername. - * - * ### Usage: - * - * ``` - * CakeLog::config('second_file', array( - * 'engine' => 'File', - * 'path' => '/var/logs/my_app/' - * )); - * ``` - * - * Will configure a FileLog instance to use the specified path. - * All options that are not `engine` are passed onto the logging adapter, - * and handled there. Any class can be configured as a logging - * adapter as long as it implements the methods in CakeLogInterface. - * - * ### Logging levels - * - * When configuring loggers, you can set which levels a logger will handle. - * This allows you to disable debug messages in production for example: - * - * ``` - * CakeLog::config('default', array( - * 'engine' => 'File', - * 'path' => LOGS, - * 'levels' => array('error', 'critical', 'alert', 'emergency') - * )); - * ``` - * - * The above logger would only log error messages or higher. Any - * other log messages would be discarded. - * - * ### Logging scopes - * - * When configuring loggers you can define the active scopes the logger - * is for. If defined only the listed scopes will be handled by the - * logger. If you don't define any scopes an adapter will catch - * all scopes that match the handled levels. - * - * ``` - * CakeLog::config('payments', array( - * 'engine' => 'File', - * 'types' => array('info', 'error', 'warning'), - * 'scopes' => array('payment', 'order') - * )); - * ``` - * - * The above logger will only capture log entries made in the - * `payment` and `order` scopes. All other scopes including the - * undefined scope will be ignored. Its important to remember that - * when using scopes you must also define the `types` of log messages - * that a logger will handle. Failing to do so will result in the logger - * catching all log messages even if the scope is incorrect. - * - * @param string $key The keyname for this logger, used to remove the - * logger later. - * @param array $config Array of configuration information for the logger - * @return bool success of configuration. - * @throws CakeLogException - * @link https://book.cakephp.org/2.0/en/core-libraries/logging.html#creating-and-configuring-log-streams - */ - public static function config($key, $config) { - if (!preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/', $key)) { - throw new CakeLogException(__d('cake_dev', 'Invalid key name')); - } - if (empty($config['engine'])) { - throw new CakeLogException(__d('cake_dev', 'Missing logger class name')); - } - if (empty(static::$_Collection)) { - static::_init(); - } - static::$_Collection->load($key, $config); - return true; - } + /** + * initialize ObjectCollection + * + * @return void + */ + protected static function _init() + { + static::$_levels = static::defaultLevels(); + static::$_Collection = new LogEngineCollection(); + } -/** - * Returns the keynames of the currently active streams - * - * @return array Array of configured log streams. - */ - public static function configured() { - if (empty(static::$_Collection)) { - static::_init(); - } - return static::$_Collection->loaded(); - } + /** + * Reset log levels to the original value + * + * @return array Default log levels + */ + public static function defaultLevels() + { + static::$_levelMap = static::$_defaultLevels; + static::$_levels = array_flip(static::$_levelMap); + return static::$_levels; + } -/** - * Gets/sets log levels - * - * Call this method without arguments, eg: `CakeLog::levels()` to obtain current - * level configuration. - * - * To append additional level 'user0' and 'user1' to to default log levels: - * - * ``` - * CakeLog::levels(array('user0, 'user1')); - * // or - * CakeLog::levels(array('user0, 'user1'), true); - * ``` - * - * will result in: - * - * ``` - * array( - * 0 => 'emergency', - * 1 => 'alert', - * ... - * 8 => 'user0', - * 9 => 'user1', - * ); - * ``` - * - * To set/replace existing configuration, pass an array with the second argument - * set to false. - * - * ``` - * CakeLog::levels(array('user0, 'user1'), false); - * ``` - * - * will result in: - * - * ``` - * array( - * 0 => 'user0', - * 1 => 'user1', - * ); - * ``` - * - * @param array $levels array - * @param bool $append true to append, false to replace - * @return array Active log levels - */ - public static function levels($levels = array(), $append = true) { - if (empty(static::$_Collection)) { - static::_init(); - } - if (empty($levels)) { - return static::$_levels; - } - $levels = array_values($levels); - if ($append) { - static::$_levels = array_merge(static::$_levels, $levels); - } else { - static::$_levels = $levels; - } - static::$_levelMap = array_flip(static::$_levels); - return static::$_levels; - } + /** + * Returns the keynames of the currently active streams + * + * @return array Array of configured log streams. + */ + public static function configured() + { + if (empty(static::$_Collection)) { + static::_init(); + } + return static::$_Collection->loaded(); + } -/** - * Reset log levels to the original value - * - * @return array Default log levels - */ - public static function defaultLevels() { - static::$_levelMap = static::$_defaultLevels; - static::$_levels = array_flip(static::$_levelMap); - return static::$_levels; - } + /** + * Gets/sets log levels + * + * Call this method without arguments, eg: `CakeLog::levels()` to obtain current + * level configuration. + * + * To append additional level 'user0' and 'user1' to to default log levels: + * + * ``` + * CakeLog::levels(array('user0, 'user1')); + * // or + * CakeLog::levels(array('user0, 'user1'), true); + * ``` + * + * will result in: + * + * ``` + * array( + * 0 => 'emergency', + * 1 => 'alert', + * ... + * 8 => 'user0', + * 9 => 'user1', + * ); + * ``` + * + * To set/replace existing configuration, pass an array with the second argument + * set to false. + * + * ``` + * CakeLog::levels(array('user0, 'user1'), false); + * ``` + * + * will result in: + * + * ``` + * array( + * 0 => 'user0', + * 1 => 'user1', + * ); + * ``` + * + * @param array $levels array + * @param bool $append true to append, false to replace + * @return array Active log levels + */ + public static function levels($levels = [], $append = true) + { + if (empty(static::$_Collection)) { + static::_init(); + } + if (empty($levels)) { + return static::$_levels; + } + $levels = array_values($levels); + if ($append) { + static::$_levels = array_merge(static::$_levels, $levels); + } else { + static::$_levels = $levels; + } + static::$_levelMap = array_flip(static::$_levels); + return static::$_levels; + } -/** - * Removes a stream from the active streams. Once a stream has been removed - * it will no longer have messages sent to it. - * - * @param string $streamName Key name of a configured stream to remove. - * @return void - */ - public static function drop($streamName) { - if (empty(static::$_Collection)) { - static::_init(); - } - static::$_Collection->unload($streamName); - } + /** + * Removes a stream from the active streams. Once a stream has been removed + * it will no longer have messages sent to it. + * + * @param string $streamName Key name of a configured stream to remove. + * @return void + */ + public static function drop($streamName) + { + if (empty(static::$_Collection)) { + static::_init(); + } + static::$_Collection->unload($streamName); + } -/** - * Checks whether $streamName is enabled - * - * @param string $streamName to check - * @return bool - * @throws CakeLogException - */ - public static function enabled($streamName) { - if (empty(static::$_Collection)) { - static::_init(); - } - if (!isset(static::$_Collection->{$streamName})) { - throw new CakeLogException(__d('cake_dev', 'Stream %s not found', $streamName)); - } - return static::$_Collection->enabled($streamName); - } + /** + * Checks whether $streamName is enabled + * + * @param string $streamName to check + * @return bool + * @throws CakeLogException + */ + public static function enabled($streamName) + { + if (empty(static::$_Collection)) { + static::_init(); + } + if (!isset(static::$_Collection->{$streamName})) { + throw new CakeLogException(__d('cake_dev', 'Stream %s not found', $streamName)); + } + return static::$_Collection->enabled($streamName); + } -/** - * Enable stream. Streams that were previously disabled - * can be re-enabled with this method. - * - * @param string $streamName to enable - * @return void - * @throws CakeLogException - */ - public static function enable($streamName) { - if (empty(static::$_Collection)) { - static::_init(); - } - if (!isset(static::$_Collection->{$streamName})) { - throw new CakeLogException(__d('cake_dev', 'Stream %s not found', $streamName)); - } - static::$_Collection->enable($streamName); - } + /** + * Enable stream. Streams that were previously disabled + * can be re-enabled with this method. + * + * @param string $streamName to enable + * @return void + * @throws CakeLogException + */ + public static function enable($streamName) + { + if (empty(static::$_Collection)) { + static::_init(); + } + if (!isset(static::$_Collection->{$streamName})) { + throw new CakeLogException(__d('cake_dev', 'Stream %s not found', $streamName)); + } + static::$_Collection->enable($streamName); + } -/** - * Disable stream. Disabling a stream will - * prevent that log stream from receiving any messages until - * its re-enabled. - * - * @param string $streamName to disable - * @return void - * @throws CakeLogException - */ - public static function disable($streamName) { - if (empty(static::$_Collection)) { - static::_init(); - } - if (!isset(static::$_Collection->{$streamName})) { - throw new CakeLogException(__d('cake_dev', 'Stream %s not found', $streamName)); - } - static::$_Collection->disable($streamName); - } + /** + * Disable stream. Disabling a stream will + * prevent that log stream from receiving any messages until + * its re-enabled. + * + * @param string $streamName to disable + * @return void + * @throws CakeLogException + */ + public static function disable($streamName) + { + if (empty(static::$_Collection)) { + static::_init(); + } + if (!isset(static::$_Collection->{$streamName})) { + throw new CakeLogException(__d('cake_dev', 'Stream %s not found', $streamName)); + } + static::$_Collection->disable($streamName); + } -/** - * Gets the logging engine from the active streams. - * - * @param string $streamName Key name of a configured stream to get. - * @return mixed instance of BaseLog or false if not found - * @see BaseLog - */ - public static function stream($streamName) { - if (empty(static::$_Collection)) { - static::_init(); - } - if (!empty(static::$_Collection->{$streamName})) { - return static::$_Collection->{$streamName}; - } - return false; - } + /** + * Gets the logging engine from the active streams. + * + * @param string $streamName Key name of a configured stream to get. + * @return mixed instance of BaseLog or false if not found + * @see BaseLog + */ + public static function stream($streamName) + { + if (empty(static::$_Collection)) { + static::_init(); + } + if (!empty(static::$_Collection->{$streamName})) { + return static::$_Collection->{$streamName}; + } + return false; + } -/** - * Writes the given message and type to all of the configured log adapters. - * Configured adapters are passed both the $type and $message variables. $type - * is one of the following strings/values. - * - * ### Types: - * - * - LOG_EMERG => 'emergency', - * - LOG_ALERT => 'alert', - * - LOG_CRIT => 'critical', - * - `LOG_ERR` => 'error', - * - `LOG_WARNING` => 'warning', - * - `LOG_NOTICE` => 'notice', - * - `LOG_INFO` => 'info', - * - `LOG_DEBUG` => 'debug', - * - * ### Usage: - * - * Write a message to the 'warning' log: - * - * `CakeLog::write('warning', 'Stuff is broken here');` - * - * @param int|string $type Type of message being written. When value is an integer - * or a string matching the recognized levels, then it will - * be treated as a log level. Otherwise it's treated as a scope. - * @param string $message Message content to log - * @param string|array $scope The scope(s) a log message is being created in. - * See CakeLog::config() for more information on logging scopes. - * @return bool Success - * @link https://book.cakephp.org/2.0/en/core-libraries/logging.html#writing-to-logs - */ - public static function write($type, $message, $scope = array()) { - if (empty(static::$_Collection)) { - static::_init(); - } + /** + * Convenience method to log emergency messages + * + * @param string $message log message + * @param string|array $scope The scope(s) a log message is being created in. + * See CakeLog::config() for more information on logging scopes. + * @return bool Success + */ + public static function emergency($message, $scope = []) + { + return static::write(static::$_levelMap['emergency'], $message, $scope); + } - if (is_int($type) && isset(static::$_levels[$type])) { - $type = static::$_levels[$type]; - } - if (is_string($type) && empty($scope) && !in_array($type, static::$_levels)) { - $scope = $type; - } - $logged = false; - foreach (static::$_Collection->enabled() as $streamName) { - $logger = static::$_Collection->{$streamName}; - $types = $scopes = $config = array(); - if (method_exists($logger, 'config')) { - $config = $logger->config(); - } - if (isset($config['types'])) { - $types = $config['types']; - } - if (isset($config['scopes'])) { - $scopes = $config['scopes']; - } - $inScope = (count(array_intersect((array)$scope, $scopes)) > 0); - $correctLevel = in_array($type, $types); + /** + * Writes the given message and type to all of the configured log adapters. + * Configured adapters are passed both the $type and $message variables. $type + * is one of the following strings/values. + * + * ### Types: + * + * - LOG_EMERG => 'emergency', + * - LOG_ALERT => 'alert', + * - LOG_CRIT => 'critical', + * - `LOG_ERR` => 'error', + * - `LOG_WARNING` => 'warning', + * - `LOG_NOTICE` => 'notice', + * - `LOG_INFO` => 'info', + * - `LOG_DEBUG` => 'debug', + * + * ### Usage: + * + * Write a message to the 'warning' log: + * + * `CakeLog::write('warning', 'Stuff is broken here');` + * + * @param int|string $type Type of message being written. When value is an integer + * or a string matching the recognized levels, then it will + * be treated as a log level. Otherwise it's treated as a scope. + * @param string $message Message content to log + * @param string|array $scope The scope(s) a log message is being created in. + * See CakeLog::config() for more information on logging scopes. + * @return bool Success + * @link https://book.cakephp.org/2.0/en/core-libraries/logging.html#writing-to-logs + */ + public static function write($type, $message, $scope = []) + { + if (empty(static::$_Collection)) { + static::_init(); + } - if ( - // No config is a catch all (bc mode) - (empty($types) && empty($scopes)) || - // BC layer for mixing scope & level - (in_array($type, $scopes)) || - // no scopes, but has level - (empty($scopes) && $correctLevel) || - // exact scope + level - ($correctLevel && $inScope) - ) { - $logger->write($type, $message); - $logged = true; - } - } - return $logged; - } + if (is_int($type) && isset(static::$_levels[$type])) { + $type = static::$_levels[$type]; + } + if (is_string($type) && empty($scope) && !in_array($type, static::$_levels)) { + $scope = $type; + } + $logged = false; + foreach (static::$_Collection->enabled() as $streamName) { + $logger = static::$_Collection->{$streamName}; + $types = $scopes = $config = []; + if (method_exists($logger, 'config')) { + $config = $logger->config(); + } + if (isset($config['types'])) { + $types = $config['types']; + } + if (isset($config['scopes'])) { + $scopes = $config['scopes']; + } + $inScope = (count(array_intersect((array)$scope, $scopes)) > 0); + $correctLevel = in_array($type, $types); -/** - * Convenience method to log emergency messages - * - * @param string $message log message - * @param string|array $scope The scope(s) a log message is being created in. - * See CakeLog::config() for more information on logging scopes. - * @return bool Success - */ - public static function emergency($message, $scope = array()) { - return static::write(static::$_levelMap['emergency'], $message, $scope); - } + if ( + // No config is a catch all (bc mode) + (empty($types) && empty($scopes)) || + // BC layer for mixing scope & level + (in_array($type, $scopes)) || + // no scopes, but has level + (empty($scopes) && $correctLevel) || + // exact scope + level + ($correctLevel && $inScope) + ) { + $logger->write($type, $message); + $logged = true; + } + } + return $logged; + } -/** - * Convenience method to log alert messages - * - * @param string $message log message - * @param string|array $scope The scope(s) a log message is being created in. - * See CakeLog::config() for more information on logging scopes. - * @return bool Success - */ - public static function alert($message, $scope = array()) { - return static::write(static::$_levelMap['alert'], $message, $scope); - } + /** + * Convenience method to log alert messages + * + * @param string $message log message + * @param string|array $scope The scope(s) a log message is being created in. + * See CakeLog::config() for more information on logging scopes. + * @return bool Success + */ + public static function alert($message, $scope = []) + { + return static::write(static::$_levelMap['alert'], $message, $scope); + } -/** - * Convenience method to log critical messages - * - * @param string $message log message - * @param string|array $scope The scope(s) a log message is being created in. - * See CakeLog::config() for more information on logging scopes. - * @return bool Success - */ - public static function critical($message, $scope = array()) { - return static::write(static::$_levelMap['critical'], $message, $scope); - } + /** + * Convenience method to log critical messages + * + * @param string $message log message + * @param string|array $scope The scope(s) a log message is being created in. + * See CakeLog::config() for more information on logging scopes. + * @return bool Success + */ + public static function critical($message, $scope = []) + { + return static::write(static::$_levelMap['critical'], $message, $scope); + } -/** - * Convenience method to log error messages - * - * @param string $message log message - * @param string|array $scope The scope(s) a log message is being created in. - * See CakeLog::config() for more information on logging scopes. - * @return bool Success - */ - public static function error($message, $scope = array()) { - return static::write(static::$_levelMap['error'], $message, $scope); - } + /** + * Convenience method to log error messages + * + * @param string $message log message + * @param string|array $scope The scope(s) a log message is being created in. + * See CakeLog::config() for more information on logging scopes. + * @return bool Success + */ + public static function error($message, $scope = []) + { + return static::write(static::$_levelMap['error'], $message, $scope); + } -/** - * Convenience method to log warning messages - * - * @param string $message log message - * @param string|array $scope The scope(s) a log message is being created in. - * See CakeLog::config() for more information on logging scopes. - * @return bool Success - */ - public static function warning($message, $scope = array()) { - return static::write(static::$_levelMap['warning'], $message, $scope); - } + /** + * Convenience method to log warning messages + * + * @param string $message log message + * @param string|array $scope The scope(s) a log message is being created in. + * See CakeLog::config() for more information on logging scopes. + * @return bool Success + */ + public static function warning($message, $scope = []) + { + return static::write(static::$_levelMap['warning'], $message, $scope); + } -/** - * Convenience method to log notice messages - * - * @param string $message log message - * @param string|array $scope The scope(s) a log message is being created in. - * See CakeLog::config() for more information on logging scopes. - * @return bool Success - */ - public static function notice($message, $scope = array()) { - return static::write(static::$_levelMap['notice'], $message, $scope); - } + /** + * Convenience method to log notice messages + * + * @param string $message log message + * @param string|array $scope The scope(s) a log message is being created in. + * See CakeLog::config() for more information on logging scopes. + * @return bool Success + */ + public static function notice($message, $scope = []) + { + return static::write(static::$_levelMap['notice'], $message, $scope); + } -/** - * Convenience method to log debug messages - * - * @param string $message log message - * @param string|array $scope The scope(s) a log message is being created in. - * See CakeLog::config() for more information on logging scopes. - * @return bool Success - */ - public static function debug($message, $scope = array()) { - return static::write(static::$_levelMap['debug'], $message, $scope); - } + /** + * Convenience method to log debug messages + * + * @param string $message log message + * @param string|array $scope The scope(s) a log message is being created in. + * See CakeLog::config() for more information on logging scopes. + * @return bool Success + */ + public static function debug($message, $scope = []) + { + return static::write(static::$_levelMap['debug'], $message, $scope); + } -/** - * Convenience method to log info messages - * - * @param string $message log message - * @param string|array $scope The scope(s) a log message is being created in. - * See CakeLog::config() for more information on logging scopes. - * @return bool Success - */ - public static function info($message, $scope = array()) { - return static::write(static::$_levelMap['info'], $message, $scope); - } + /** + * Convenience method to log info messages + * + * @param string $message log message + * @param string|array $scope The scope(s) a log message is being created in. + * See CakeLog::config() for more information on logging scopes. + * @return bool Success + */ + public static function info($message, $scope = []) + { + return static::write(static::$_levelMap['info'], $message, $scope); + } } diff --git a/lib/Cake/Log/CakeLogInterface.php b/lib/Cake/Log/CakeLogInterface.php index b8e73b6b..fe47250d 100755 --- a/lib/Cake/Log/CakeLogInterface.php +++ b/lib/Cake/Log/CakeLogInterface.php @@ -22,15 +22,16 @@ * * @package Cake.Log */ -interface CakeLogInterface { +interface CakeLogInterface +{ -/** - * Write method to handle writes being made to the Logger - * - * @param string $type Message type. - * @param string $message Message to write. - * @return void - */ - public function write($type, $message); + /** + * Write method to handle writes being made to the Logger + * + * @param string $type Message type. + * @param string $message Message to write. + * @return void + */ + public function write($type, $message); } diff --git a/lib/Cake/Log/Engine/BaseLog.php b/lib/Cake/Log/Engine/BaseLog.php index f2ad6e14..dc91c559 100755 --- a/lib/Cake/Log/Engine/BaseLog.php +++ b/lib/Cake/Log/Engine/BaseLog.php @@ -23,45 +23,48 @@ * * @package Cake.Log.Engine */ -abstract class BaseLog implements CakeLogInterface { +abstract class BaseLog implements CakeLogInterface +{ -/** - * Engine config - * - * @var string - */ - protected $_config = array(); + /** + * Engine config + * + * @var string + */ + protected $_config = []; -/** - * Constructor - * - * @param array $config Configuration array - */ - public function __construct($config = array()) { - $this->config($config); - } + /** + * Constructor + * + * @param array $config Configuration array + */ + public function __construct($config = []) + { + $this->config($config); + } -/** - * Sets instance config. When $config is null, returns config array - * - * Config - * - * - `types` string or array, levels the engine is interested in - * - `scopes` string or array, scopes the engine is interested in - * - * @param array $config engine configuration - * @return array - */ - public function config($config = array()) { - if (!empty($config)) { - foreach (array('types', 'scopes') as $option) { - if (isset($config[$option]) && is_string($config[$option])) { - $config[$option] = array($config[$option]); - } - } - $this->_config = $config; - } - return $this->_config; - } + /** + * Sets instance config. When $config is null, returns config array + * + * Config + * + * - `types` string or array, levels the engine is interested in + * - `scopes` string or array, scopes the engine is interested in + * + * @param array $config engine configuration + * @return array + */ + public function config($config = []) + { + if (!empty($config)) { + foreach (['types', 'scopes'] as $option) { + if (isset($config[$option]) && is_string($config[$option])) { + $config[$option] = [$config[$option]]; + } + } + $this->_config = $config; + } + return $this->_config; + } } diff --git a/lib/Cake/Log/Engine/ConsoleLog.php b/lib/Cake/Log/Engine/ConsoleLog.php index ced97e63..6e7b39e3 100755 --- a/lib/Cake/Log/Engine/ConsoleLog.php +++ b/lib/Cake/Log/Engine/ConsoleLog.php @@ -24,64 +24,67 @@ * * @package Cake.Log.Engine */ -class ConsoleLog extends BaseLog { +class ConsoleLog extends BaseLog +{ -/** - * Output stream - * - * @var ConsoleOutput - */ - protected $_output = null; + /** + * Output stream + * + * @var ConsoleOutput + */ + protected $_output = null; -/** - * Constructs a new Console Logger. - * - * Config - * - * - `types` string or array, levels the engine is interested in - * - `scopes` string or array, scopes the engine is interested in - * - `stream` the path to save logs on. - * - `outputAs` integer or ConsoleOutput::[RAW|PLAIN|COLOR] - * - * @param array $config Options for the FileLog, see above. - * @throws CakeLogException - */ - public function __construct($config = array()) { - parent::__construct($config); - if ((DS === '\\' && !(bool)env('ANSICON') && env('ConEmuANSI') !== 'ON') || - (function_exists('posix_isatty') && !posix_isatty($this->_output)) - ) { - $outputAs = ConsoleOutput::PLAIN; - } else { - $outputAs = ConsoleOutput::COLOR; - } - $config = Hash::merge(array( - 'stream' => 'php://stderr', - 'types' => null, - 'scopes' => array(), - 'outputAs' => $outputAs, - ), $this->_config); - $config = $this->config($config); - if ($config['stream'] instanceof ConsoleOutput) { - $this->_output = $config['stream']; - } elseif (is_string($config['stream'])) { - $this->_output = new ConsoleOutput($config['stream']); - } else { - throw new CakeLogException('`stream` not a ConsoleOutput nor string'); - } - $this->_output->outputAs($config['outputAs']); - } + /** + * Constructs a new Console Logger. + * + * Config + * + * - `types` string or array, levels the engine is interested in + * - `scopes` string or array, scopes the engine is interested in + * - `stream` the path to save logs on. + * - `outputAs` integer or ConsoleOutput::[RAW|PLAIN|COLOR] + * + * @param array $config Options for the FileLog, see above. + * @throws CakeLogException + */ + public function __construct($config = []) + { + parent::__construct($config); + if ((DS === '\\' && !(bool)env('ANSICON') && env('ConEmuANSI') !== 'ON') || + (function_exists('posix_isatty') && !posix_isatty($this->_output)) + ) { + $outputAs = ConsoleOutput::PLAIN; + } else { + $outputAs = ConsoleOutput::COLOR; + } + $config = Hash::merge([ + 'stream' => 'php://stderr', + 'types' => null, + 'scopes' => [], + 'outputAs' => $outputAs, + ], $this->_config); + $config = $this->config($config); + if ($config['stream'] instanceof ConsoleOutput) { + $this->_output = $config['stream']; + } else if (is_string($config['stream'])) { + $this->_output = new ConsoleOutput($config['stream']); + } else { + throw new CakeLogException('`stream` not a ConsoleOutput nor string'); + } + $this->_output->outputAs($config['outputAs']); + } -/** - * Implements writing to console. - * - * @param string $type The type of log you are making. - * @param string $message The message you want to log. - * @return bool success of write. - */ - public function write($type, $message) { - $output = date('Y-m-d H:i:s') . ' ' . ucfirst($type) . ': ' . $message . "\n"; - return $this->_output->write(sprintf('<%s>%s', $type, $output, $type), false); - } + /** + * Implements writing to console. + * + * @param string $type The type of log you are making. + * @param string $message The message you want to log. + * @return bool success of write. + */ + public function write($type, $message) + { + $output = date('Y-m-d H:i:s') . ' ' . ucfirst($type) . ': ' . $message . "\n"; + return $this->_output->write(sprintf('<%s>%s', $type, $output, $type), false); + } } diff --git a/lib/Cake/Log/Engine/FileLog.php b/lib/Cake/Log/Engine/FileLog.php index c337ca76..7e5490e2 100755 --- a/lib/Cake/Log/Engine/FileLog.php +++ b/lib/Cake/Log/Engine/FileLog.php @@ -26,198 +26,204 @@ * * @package Cake.Log.Engine */ -class FileLog extends BaseLog { +class FileLog extends BaseLog +{ + + /** + * Default configuration values + * + * @var array + * @see FileLog::__construct() + */ + protected $_defaults = [ + 'path' => LOGS, + 'file' => null, + 'types' => null, + 'scopes' => [], + 'rotate' => 10, + 'size' => 10485760, // 10MB + 'mask' => null, + ]; + + /** + * Path to save log files on. + * + * @var string + */ + protected $_path = null; + + /** + * Log file name + * + * @var string + */ + protected $_file = null; + + /** + * Max file size, used for log file rotation. + * + * @var int + */ + protected $_size = null; + + /** + * Constructs a new File Logger. + * + * Config + * + * - `types` string or array, levels the engine is interested in + * - `scopes` string or array, scopes the engine is interested in + * - `file` Log file name + * - `path` The path to save logs on. + * - `size` Used to implement basic log file rotation. If log file size + * reaches specified size the existing file is renamed by appending timestamp + * to filename and new log file is created. Can be integer bytes value or + * human reabable string values like '10MB', '100KB' etc. + * - `rotate` Log files are rotated specified times before being removed. + * If value is 0, old versions are removed rather then rotated. + * - `mask` A mask is applied when log files are created. Left empty no chmod + * is made. + * + * @param array $config Options for the FileLog, see above. + */ + public function __construct($config = []) + { + $config = Hash::merge($this->_defaults, $config); + parent::__construct($config); + } + + /** + * Sets protected properties based on config provided + * + * @param array $config Engine configuration + * @return array + */ + public function config($config = []) + { + parent::config($config); + + if (!empty($config['path'])) { + $this->_path = $config['path']; + } + if (Configure::read('debug') && !is_dir($this->_path)) { + mkdir($this->_path, 0775, true); + } -/** - * Default configuration values - * - * @var array - * @see FileLog::__construct() - */ - protected $_defaults = array( - 'path' => LOGS, - 'file' => null, - 'types' => null, - 'scopes' => array(), - 'rotate' => 10, - 'size' => 10485760, // 10MB - 'mask' => null, - ); + if (!empty($config['file'])) { + $this->_file = $config['file']; + if (substr($this->_file, -4) !== '.log') { + $this->_file .= '.log'; + } + } + if (!empty($config['size'])) { + if (is_numeric($config['size'])) { + $this->_size = (int)$config['size']; + } else { + $this->_size = CakeNumber::fromReadableSize($config['size']); + } + } -/** - * Path to save log files on. - * - * @var string - */ - protected $_path = null; + return $this->_config; + } + + /** + * Implements writing to log files. + * + * @param string $type The type of log you are making. + * @param string $message The message you want to log. + * @return bool success of write. + */ + public function write($type, $message) + { + $output = date('Y-m-d H:i:s') . ' ' . ucfirst($type) . ': ' . $message . "\n"; + $filename = $this->_getFilename($type); + if (!empty($this->_size)) { + $this->_rotateFile($filename); + } -/** - * Log file name - * - * @var string - */ - protected $_file = null; + $pathname = $this->_path . $filename; + if (!is_dir($this->_path)) { + mkdir($this->_path, 0755, true); + } + if (empty($this->_config['mask'])) { + return file_put_contents($pathname, $output, FILE_APPEND); + } -/** - * Max file size, used for log file rotation. - * - * @var int - */ - protected $_size = null; + $exists = file_exists($pathname); + $result = file_put_contents($pathname, $output, FILE_APPEND); + static $selfError = false; + if (!$selfError && !$exists && !chmod($pathname, (int)$this->_config['mask'])) { + $selfError = true; + trigger_error(__d( + 'cake_dev', 'Could not apply permission mask "%s" on log file "%s"', + [$this->_config['mask'], $pathname]), E_USER_WARNING); + $selfError = false; + } + return $result; + } + + /** + * Get filename + * + * @param string $type The type of log. + * @return string File name + */ + protected function _getFilename($type) + { + $debugTypes = ['notice', 'info', 'debug']; + + if (!empty($this->_file)) { + $filename = $this->_file; + } else if ($type === 'error' || $type === 'warning') { + $filename = 'error.log'; + } else if (in_array($type, $debugTypes)) { + $filename = 'debug.log'; + } else { + $filename = $type . '.log'; + } -/** - * Constructs a new File Logger. - * - * Config - * - * - `types` string or array, levels the engine is interested in - * - `scopes` string or array, scopes the engine is interested in - * - `file` Log file name - * - `path` The path to save logs on. - * - `size` Used to implement basic log file rotation. If log file size - * reaches specified size the existing file is renamed by appending timestamp - * to filename and new log file is created. Can be integer bytes value or - * human reabable string values like '10MB', '100KB' etc. - * - `rotate` Log files are rotated specified times before being removed. - * If value is 0, old versions are removed rather then rotated. - * - `mask` A mask is applied when log files are created. Left empty no chmod - * is made. - * - * @param array $config Options for the FileLog, see above. - */ - public function __construct($config = array()) { - $config = Hash::merge($this->_defaults, $config); - parent::__construct($config); - } + return $filename; + } + + /** + * Rotate log file if size specified in config is reached. + * Also if `rotate` count is reached oldest file is removed. + * + * @param string $filename Log file name + * @return mixed True if rotated successfully or false in case of error, otherwise null. + * Void if file doesn't need to be rotated. + */ + protected function _rotateFile($filename) + { + $filepath = $this->_path . $filename; + if (version_compare(PHP_VERSION, '5.3.0') >= 0) { + clearstatcache(true, $filepath); + } else { + clearstatcache(); + } -/** - * Sets protected properties based on config provided - * - * @param array $config Engine configuration - * @return array - */ - public function config($config = array()) { - parent::config($config); - - if (!empty($config['path'])) { - $this->_path = $config['path']; - } - if (Configure::read('debug') && !is_dir($this->_path)) { - mkdir($this->_path, 0775, true); - } - - if (!empty($config['file'])) { - $this->_file = $config['file']; - if (substr($this->_file, -4) !== '.log') { - $this->_file .= '.log'; - } - } - if (!empty($config['size'])) { - if (is_numeric($config['size'])) { - $this->_size = (int)$config['size']; - } else { - $this->_size = CakeNumber::fromReadableSize($config['size']); - } - } - - return $this->_config; - } + if (!file_exists($filepath) || + filesize($filepath) < $this->_size + ) { + return null; + } -/** - * Implements writing to log files. - * - * @param string $type The type of log you are making. - * @param string $message The message you want to log. - * @return bool success of write. - */ - public function write($type, $message) { - $output = date('Y-m-d H:i:s') . ' ' . ucfirst($type) . ': ' . $message . "\n"; - $filename = $this->_getFilename($type); - if (!empty($this->_size)) { - $this->_rotateFile($filename); - } - - $pathname = $this->_path . $filename; - if(!is_dir($this->_path)) { - mkdir($this->_path, 0755, true); + if ($this->_config['rotate'] === 0) { + $result = unlink($filepath); + } else { + $result = rename($filepath, $filepath . '.' . time()); } - if (empty($this->_config['mask'])) { - return file_put_contents($pathname, $output, FILE_APPEND); - } - - $exists = file_exists($pathname); - $result = file_put_contents($pathname, $output, FILE_APPEND); - static $selfError = false; - if (!$selfError && !$exists && !chmod($pathname, (int)$this->_config['mask'])) { - $selfError = true; - trigger_error(__d( - 'cake_dev', 'Could not apply permission mask "%s" on log file "%s"', - array($this->_config['mask'], $pathname)), E_USER_WARNING); - $selfError = false; - } - return $result; - } -/** - * Get filename - * - * @param string $type The type of log. - * @return string File name - */ - protected function _getFilename($type) { - $debugTypes = array('notice', 'info', 'debug'); - - if (!empty($this->_file)) { - $filename = $this->_file; - } elseif ($type === 'error' || $type === 'warning') { - $filename = 'error.log'; - } elseif (in_array($type, $debugTypes)) { - $filename = 'debug.log'; - } else { - $filename = $type . '.log'; - } - - return $filename; - } + $files = glob($filepath . '.*'); + if ($files) { + $filesToDelete = count($files) - $this->_config['rotate']; + while ($filesToDelete > 0) { + unlink(array_shift($files)); + $filesToDelete--; + } + } -/** - * Rotate log file if size specified in config is reached. - * Also if `rotate` count is reached oldest file is removed. - * - * @param string $filename Log file name - * @return mixed True if rotated successfully or false in case of error, otherwise null. - * Void if file doesn't need to be rotated. - */ - protected function _rotateFile($filename) { - $filepath = $this->_path . $filename; - if (version_compare(PHP_VERSION, '5.3.0') >= 0) { - clearstatcache(true, $filepath); - } else { - clearstatcache(); - } - - if (!file_exists($filepath) || - filesize($filepath) < $this->_size - ) { - return null; - } - - if ($this->_config['rotate'] === 0) { - $result = unlink($filepath); - } else { - $result = rename($filepath, $filepath . '.' . time()); - } - - $files = glob($filepath . '.*'); - if ($files) { - $filesToDelete = count($files) - $this->_config['rotate']; - while ($filesToDelete > 0) { - unlink(array_shift($files)); - $filesToDelete--; - } - } - - return $result; - } + return $result; + } } diff --git a/lib/Cake/Log/Engine/SyslogLog.php b/lib/Cake/Log/Engine/SyslogLog.php index d20841e2..afa14c27 100755 --- a/lib/Cake/Log/Engine/SyslogLog.php +++ b/lib/Cake/Log/Engine/SyslogLog.php @@ -23,136 +23,142 @@ * * @package Cake.Log.Engine */ -class SyslogLog extends BaseLog { +class SyslogLog extends BaseLog +{ -/** - * By default messages are formatted as: - * type: message - * - * To override the log format (e.g. to add your own info) define the format key when configuring - * this logger - * - * If you wish to include a prefix to all messages, for instance to identify the - * application or the web server, then use the prefix option. Please keep in mind - * the prefix is shared by all streams using syslog, as it is dependent of - * the running process. For a local prefix, to be used only by one stream, you - * can use the format key. - * - * ## Example: - * - * ``` - * CakeLog::config('error', array( - * 'engine' => 'Syslog', - * 'types' => array('emergency', 'alert', 'critical', 'error'), - * 'format' => "%s: My-App - %s", - * 'prefix' => 'Web Server 01' - * )); - * ``` - * - * @var array - */ - protected $_defaults = array( - 'format' => '%s: %s', - 'flag' => LOG_ODELAY, - 'prefix' => '', - 'facility' => LOG_USER - ); + /** + * By default messages are formatted as: + * type: message + * + * To override the log format (e.g. to add your own info) define the format key when configuring + * this logger + * + * If you wish to include a prefix to all messages, for instance to identify the + * application or the web server, then use the prefix option. Please keep in mind + * the prefix is shared by all streams using syslog, as it is dependent of + * the running process. For a local prefix, to be used only by one stream, you + * can use the format key. + * + * ## Example: + * + * ``` + * CakeLog::config('error', array( + * 'engine' => 'Syslog', + * 'types' => array('emergency', 'alert', 'critical', 'error'), + * 'format' => "%s: My-App - %s", + * 'prefix' => 'Web Server 01' + * )); + * ``` + * + * @var array + */ + protected $_defaults = [ + 'format' => '%s: %s', + 'flag' => LOG_ODELAY, + 'prefix' => '', + 'facility' => LOG_USER + ]; -/** - * Used to map the string names back to their LOG_* constants - * - * @var array - */ - protected $_priorityMap = array( - 'emergency' => LOG_EMERG, - 'alert' => LOG_ALERT, - 'critical' => LOG_CRIT, - 'error' => LOG_ERR, - 'warning' => LOG_WARNING, - 'notice' => LOG_NOTICE, - 'info' => LOG_INFO, - 'debug' => LOG_DEBUG - ); + /** + * Used to map the string names back to their LOG_* constants + * + * @var array + */ + protected $_priorityMap = [ + 'emergency' => LOG_EMERG, + 'alert' => LOG_ALERT, + 'critical' => LOG_CRIT, + 'error' => LOG_ERR, + 'warning' => LOG_WARNING, + 'notice' => LOG_NOTICE, + 'info' => LOG_INFO, + 'debug' => LOG_DEBUG + ]; -/** - * Whether the logger connection is open or not - * - * @var bool - */ - protected $_open = false; + /** + * Whether the logger connection is open or not + * + * @var bool + */ + protected $_open = false; -/** - * Make sure the configuration contains the format parameter, by default it uses - * the error number and the type as a prefix to the message - * - * @param array $config Options list. - */ - public function __construct($config = array()) { - $config += $this->_defaults; - parent::__construct($config); - } + /** + * Make sure the configuration contains the format parameter, by default it uses + * the error number and the type as a prefix to the message + * + * @param array $config Options list. + */ + public function __construct($config = []) + { + $config += $this->_defaults; + parent::__construct($config); + } -/** - * Writes a message to syslog - * - * Map the $type back to a LOG_ constant value, split multi-line messages into multiple - * log messages, pass all messages through the format defined in the configuration - * - * @param string $type The type of log you are making. - * @param string $message The message you want to log. - * @return bool success of write. - */ - public function write($type, $message) { - if (!$this->_open) { - $config = $this->_config; - $this->_open($config['prefix'], $config['flag'], $config['facility']); - $this->_open = true; - } + /** + * Writes a message to syslog + * + * Map the $type back to a LOG_ constant value, split multi-line messages into multiple + * log messages, pass all messages through the format defined in the configuration + * + * @param string $type The type of log you are making. + * @param string $message The message you want to log. + * @return bool success of write. + */ + public function write($type, $message) + { + if (!$this->_open) { + $config = $this->_config; + $this->_open($config['prefix'], $config['flag'], $config['facility']); + $this->_open = true; + } - $priority = LOG_DEBUG; - if (isset($this->_priorityMap[$type])) { - $priority = $this->_priorityMap[$type]; - } + $priority = LOG_DEBUG; + if (isset($this->_priorityMap[$type])) { + $priority = $this->_priorityMap[$type]; + } - $messages = explode("\n", $message); - foreach ($messages as $message) { - $message = sprintf($this->_config['format'], $type, $message); - $this->_write($priority, $message); - } + $messages = explode("\n", $message); + foreach ($messages as $message) { + $message = sprintf($this->_config['format'], $type, $message); + $this->_write($priority, $message); + } - return true; - } + return true; + } -/** - * Extracts the call to openlog() in order to run unit tests on it. This function - * will initialize the connection to the system logger - * - * @param string $ident the prefix to add to all messages logged - * @param int $options the options flags to be used for logged messages - * @param int $facility the stream or facility to log to - * @return void - */ - protected function _open($ident, $options, $facility) { - openlog($ident, $options, $facility); - } + /** + * Extracts the call to openlog() in order to run unit tests on it. This function + * will initialize the connection to the system logger + * + * @param string $ident the prefix to add to all messages logged + * @param int $options the options flags to be used for logged messages + * @param int $facility the stream or facility to log to + * @return void + */ + protected function _open($ident, $options, $facility) + { + openlog($ident, $options, $facility); + } -/** - * Extracts the call to syslog() in order to run unit tests on it. This function - * will perform the actual write in the system logger - * - * @param int $priority Message priority. - * @param string $message Message to log. - * @return bool - */ - protected function _write($priority, $message) { - return syslog($priority, $message); - } + /** + * Extracts the call to syslog() in order to run unit tests on it. This function + * will perform the actual write in the system logger + * + * @param int $priority Message priority. + * @param string $message Message to log. + * @return bool + */ + protected function _write($priority, $message) + { + return syslog($priority, $message); + } -/** - * Closes the logger connection - */ - public function __destruct() { - closelog(); - } + /** + * Closes the logger connection + */ + public function __destruct() + { + closelog(); + } } diff --git a/lib/Cake/Log/LogEngineCollection.php b/lib/Cake/Log/LogEngineCollection.php index f68a3320..1d26098b 100755 --- a/lib/Cake/Log/LogEngineCollection.php +++ b/lib/Cake/Log/LogEngineCollection.php @@ -23,52 +23,55 @@ * * @package Cake.Log */ -class LogEngineCollection extends ObjectCollection { +class LogEngineCollection extends ObjectCollection +{ -/** - * Loads/constructs a Log engine. - * - * @param string $name instance identifier - * @param array $options Setting for the Log Engine - * @return BaseLog BaseLog engine instance - * @throws CakeLogException when logger class does not implement a write method - */ - public function load($name, $options = array()) { - $enable = isset($options['enabled']) ? $options['enabled'] : true; - $loggerName = $options['engine']; - unset($options['engine']); - $className = $this->_getLogger($loggerName); - $logger = new $className($options); - if (!$logger instanceof CakeLogInterface) { - throw new CakeLogException( - __d('cake_dev', 'logger class %s does not implement a %s method.', $loggerName, 'write()') - ); - } - $this->_loaded[$name] = $logger; - if ($enable) { - $this->enable($name); - } - return $logger; - } + /** + * Loads/constructs a Log engine. + * + * @param string $name instance identifier + * @param array $options Setting for the Log Engine + * @return BaseLog BaseLog engine instance + * @throws CakeLogException when logger class does not implement a write method + */ + public function load($name, $options = []) + { + $enable = isset($options['enabled']) ? $options['enabled'] : true; + $loggerName = $options['engine']; + unset($options['engine']); + $className = $this->_getLogger($loggerName); + $logger = new $className($options); + if (!$logger instanceof CakeLogInterface) { + throw new CakeLogException( + __d('cake_dev', 'logger class %s does not implement a %s method.', $loggerName, 'write()') + ); + } + $this->_loaded[$name] = $logger; + if ($enable) { + $this->enable($name); + } + return $logger; + } -/** - * Attempts to import a logger class from the various paths it could be on. - * Checks that the logger class implements a write method as well. - * - * @param string $loggerName the plugin.className of the logger class you want to build. - * @return mixed boolean false on any failures, string of classname to use if search was successful. - * @throws CakeLogException - */ - protected static function _getLogger($loggerName) { - list($plugin, $loggerName) = pluginSplit($loggerName, true); - if (substr($loggerName, -3) !== 'Log') { - $loggerName .= 'Log'; - } - App::uses($loggerName, $plugin . 'Log/Engine'); - if (!class_exists($loggerName)) { - throw new CakeLogException(__d('cake_dev', 'Could not load class %s', $loggerName)); - } - return $loggerName; - } + /** + * Attempts to import a logger class from the various paths it could be on. + * Checks that the logger class implements a write method as well. + * + * @param string $loggerName the plugin.className of the logger class you want to build. + * @return mixed boolean false on any failures, string of classname to use if search was successful. + * @throws CakeLogException + */ + protected static function _getLogger($loggerName) + { + list($plugin, $loggerName) = pluginSplit($loggerName, true); + if (substr($loggerName, -3) !== 'Log') { + $loggerName .= 'Log'; + } + App::uses($loggerName, $plugin . 'Log/Engine'); + if (!class_exists($loggerName)) { + throw new CakeLogException(__d('cake_dev', 'Could not load class %s', $loggerName)); + } + return $loggerName; + } } diff --git a/lib/Cake/Model/AclNode.php b/lib/Cake/Model/AclNode.php index 11c9a510..091e16ff 100755 --- a/lib/Cake/Model/AclNode.php +++ b/lib/Cake/Model/AclNode.php @@ -21,168 +21,171 @@ * * @package Cake.Model */ -class AclNode extends Model { - -/** - * Explicitly disable in-memory query caching for ACL models - * - * @var bool - */ - public $cacheQueries = false; - -/** - * ACL models use the Tree behavior - * - * @var array - */ - public $actsAs = array('Tree' => array('type' => 'nested')); - -/** - * Constructor - * - * @param bool|int|string|array $id Set this ID for this model on startup, - * can also be an array of options, see above. - * @param string $table Name of database table to use. - * @param string $ds DataSource connection name. - */ - public function __construct($id = false, $table = null, $ds = null) { - $config = Configure::read('Acl.database'); - if (isset($config)) { - $this->useDbConfig = $config; - } - parent::__construct($id, $table, $ds); - } - -/** - * Retrieves the Aro/Aco node for this model - * - * @param string|array|Model $ref Array with 'model' and 'foreign_key', model object, or string value - * @return array Node found in database - * @throws CakeException when binding to a model that doesn't exist. - */ - public function node($ref = null) { - $db = $this->getDataSource(); - $type = $this->alias; - $result = null; - - if (!empty($this->useTable)) { - $table = $this->useTable; - } else { - $table = Inflector::pluralize(Inflector::underscore($type)); - } - - if (empty($ref)) { - return null; - } elseif (is_string($ref)) { - $path = explode('/', $ref); - $start = $path[0]; - unset($path[0]); - - $queryData = array( - 'conditions' => array( - $db->name("{$type}.lft") . ' <= ' . $db->name("{$type}0.lft"), - $db->name("{$type}.rght") . ' >= ' . $db->name("{$type}0.rght")), - 'fields' => array('id', 'parent_id', 'model', 'foreign_key', 'alias'), - 'joins' => array(array( - 'table' => $table, - 'alias' => "{$type}0", - 'type' => 'INNER', - 'conditions' => array("{$type}0.alias" => $start) - )), - 'order' => $db->name("{$type}.lft") . ' DESC' - ); - - $conditionsAfterJoin = array(); - - foreach ($path as $i => $alias) { - $j = $i - 1; - - $queryData['joins'][] = array( - 'table' => $table, - 'alias' => "{$type}{$i}", - 'type' => 'INNER', - 'conditions' => array( - "{$type}{$i}.alias" => $alias - ) - ); - - // it will be better if this conditions will performs after join operation - $conditionsAfterJoin[] = $db->name("{$type}{$j}.id") . ' = ' . $db->name("{$type}{$i}.parent_id"); - $conditionsAfterJoin[] = $db->name("{$type}{$i}.rght") . ' < ' . $db->name("{$type}{$j}.rght"); - $conditionsAfterJoin[] = $db->name("{$type}{$i}.lft") . ' > ' . $db->name("{$type}{$j}.lft"); - - $queryData['conditions'] = array('or' => array( - $db->name("{$type}.lft") . ' <= ' . $db->name("{$type}0.lft") . ' AND ' . $db->name("{$type}.rght") . ' >= ' . $db->name("{$type}0.rght"), - $db->name("{$type}.lft") . ' <= ' . $db->name("{$type}{$i}.lft") . ' AND ' . $db->name("{$type}.rght") . ' >= ' . $db->name("{$type}{$i}.rght")) - ); - } - $queryData['conditions'] = array_merge($queryData['conditions'], $conditionsAfterJoin); - $result = $db->read($this, $queryData, -1); - $path = array_values($path); - - if (!isset($result[0][$type]) || - (!empty($path) && $result[0][$type]['alias'] != $path[count($path) - 1]) || - (empty($path) && $result[0][$type]['alias'] != $start) - ) { - return false; - } - } elseif (is_object($ref) && $ref instanceof Model) { - $ref = array('model' => $ref->name, 'foreign_key' => $ref->id); - } elseif (is_array($ref) && !(isset($ref['model']) && isset($ref['foreign_key']))) { - $name = key($ref); - list(, $alias) = pluginSplit($name); - - $model = ClassRegistry::init(array('class' => $name, 'alias' => $alias)); - - if (empty($model)) { - throw new CakeException('cake_dev', "Model class '%s' not found in AclNode::node() when trying to bind %s object", $type, $this->alias); - } - - $tmpRef = null; - if (method_exists($model, 'bindNode')) { - $tmpRef = $model->bindNode($ref); - } - if (empty($tmpRef)) { - $ref = array('model' => $alias, 'foreign_key' => $ref[$name][$model->primaryKey]); - } else { - if (is_string($tmpRef)) { - return $this->node($tmpRef); - } - $ref = $tmpRef; - } - } - if (is_array($ref)) { - if (is_array(current($ref)) && is_string(key($ref))) { - $name = key($ref); - $ref = current($ref); - } - foreach ($ref as $key => $val) { - if (strpos($key, $type) !== 0 && strpos($key, '.') === false) { - unset($ref[$key]); - $ref["{$type}0.{$key}"] = $val; - } - } - $queryData = array( - 'conditions' => $ref, - 'fields' => array('id', 'parent_id', 'model', 'foreign_key', 'alias'), - 'joins' => array(array( - 'table' => $table, - 'alias' => "{$type}0", - 'type' => 'INNER', - 'conditions' => array( - $db->name("{$type}.lft") . ' <= ' . $db->name("{$type}0.lft"), - $db->name("{$type}.rght") . ' >= ' . $db->name("{$type}0.rght") - ) - )), - 'order' => $db->name("{$type}.lft") . ' DESC' - ); - $result = $db->read($this, $queryData, -1); - - if (!$result) { - throw new CakeException(__d('cake_dev', "AclNode::node() - Couldn't find %s node identified by \"%s\"", $type, print_r($ref, true))); - } - } - return $result; - } +class AclNode extends Model +{ + + /** + * Explicitly disable in-memory query caching for ACL models + * + * @var bool + */ + public $cacheQueries = false; + + /** + * ACL models use the Tree behavior + * + * @var array + */ + public $actsAs = ['Tree' => ['type' => 'nested']]; + + /** + * Constructor + * + * @param bool|int|string|array $id Set this ID for this model on startup, + * can also be an array of options, see above. + * @param string $table Name of database table to use. + * @param string $ds DataSource connection name. + */ + public function __construct($id = false, $table = null, $ds = null) + { + $config = Configure::read('Acl.database'); + if (isset($config)) { + $this->useDbConfig = $config; + } + parent::__construct($id, $table, $ds); + } + + /** + * Retrieves the Aro/Aco node for this model + * + * @param string|array|Model $ref Array with 'model' and 'foreign_key', model object, or string value + * @return array Node found in database + * @throws CakeException when binding to a model that doesn't exist. + */ + public function node($ref = null) + { + $db = $this->getDataSource(); + $type = $this->alias; + $result = null; + + if (!empty($this->useTable)) { + $table = $this->useTable; + } else { + $table = Inflector::pluralize(Inflector::underscore($type)); + } + + if (empty($ref)) { + return null; + } else if (is_string($ref)) { + $path = explode('/', $ref); + $start = $path[0]; + unset($path[0]); + + $queryData = [ + 'conditions' => [ + $db->name("{$type}.lft") . ' <= ' . $db->name("{$type}0.lft"), + $db->name("{$type}.rght") . ' >= ' . $db->name("{$type}0.rght")], + 'fields' => ['id', 'parent_id', 'model', 'foreign_key', 'alias'], + 'joins' => [[ + 'table' => $table, + 'alias' => "{$type}0", + 'type' => 'INNER', + 'conditions' => ["{$type}0.alias" => $start] + ]], + 'order' => $db->name("{$type}.lft") . ' DESC' + ]; + + $conditionsAfterJoin = []; + + foreach ($path as $i => $alias) { + $j = $i - 1; + + $queryData['joins'][] = [ + 'table' => $table, + 'alias' => "{$type}{$i}", + 'type' => 'INNER', + 'conditions' => [ + "{$type}{$i}.alias" => $alias + ] + ]; + + // it will be better if this conditions will performs after join operation + $conditionsAfterJoin[] = $db->name("{$type}{$j}.id") . ' = ' . $db->name("{$type}{$i}.parent_id"); + $conditionsAfterJoin[] = $db->name("{$type}{$i}.rght") . ' < ' . $db->name("{$type}{$j}.rght"); + $conditionsAfterJoin[] = $db->name("{$type}{$i}.lft") . ' > ' . $db->name("{$type}{$j}.lft"); + + $queryData['conditions'] = ['or' => [ + $db->name("{$type}.lft") . ' <= ' . $db->name("{$type}0.lft") . ' AND ' . $db->name("{$type}.rght") . ' >= ' . $db->name("{$type}0.rght"), + $db->name("{$type}.lft") . ' <= ' . $db->name("{$type}{$i}.lft") . ' AND ' . $db->name("{$type}.rght") . ' >= ' . $db->name("{$type}{$i}.rght")] + ]; + } + $queryData['conditions'] = array_merge($queryData['conditions'], $conditionsAfterJoin); + $result = $db->read($this, $queryData, -1); + $path = array_values($path); + + if (!isset($result[0][$type]) || + (!empty($path) && $result[0][$type]['alias'] != $path[count($path) - 1]) || + (empty($path) && $result[0][$type]['alias'] != $start) + ) { + return false; + } + } else if (is_object($ref) && $ref instanceof Model) { + $ref = ['model' => $ref->name, 'foreign_key' => $ref->id]; + } else if (is_array($ref) && !(isset($ref['model']) && isset($ref['foreign_key']))) { + $name = key($ref); + list(, $alias) = pluginSplit($name); + + $model = ClassRegistry::init(['class' => $name, 'alias' => $alias]); + + if (empty($model)) { + throw new CakeException('cake_dev', "Model class '%s' not found in AclNode::node() when trying to bind %s object", $type, $this->alias); + } + + $tmpRef = null; + if (method_exists($model, 'bindNode')) { + $tmpRef = $model->bindNode($ref); + } + if (empty($tmpRef)) { + $ref = ['model' => $alias, 'foreign_key' => $ref[$name][$model->primaryKey]]; + } else { + if (is_string($tmpRef)) { + return $this->node($tmpRef); + } + $ref = $tmpRef; + } + } + if (is_array($ref)) { + if (is_array(current($ref)) && is_string(key($ref))) { + $name = key($ref); + $ref = current($ref); + } + foreach ($ref as $key => $val) { + if (strpos($key, $type) !== 0 && strpos($key, '.') === false) { + unset($ref[$key]); + $ref["{$type}0.{$key}"] = $val; + } + } + $queryData = [ + 'conditions' => $ref, + 'fields' => ['id', 'parent_id', 'model', 'foreign_key', 'alias'], + 'joins' => [[ + 'table' => $table, + 'alias' => "{$type}0", + 'type' => 'INNER', + 'conditions' => [ + $db->name("{$type}.lft") . ' <= ' . $db->name("{$type}0.lft"), + $db->name("{$type}.rght") . ' >= ' . $db->name("{$type}0.rght") + ] + ]], + 'order' => $db->name("{$type}.lft") . ' DESC' + ]; + $result = $db->read($this, $queryData, -1); + + if (!$result) { + throw new CakeException(__d('cake_dev', "AclNode::node() - Couldn't find %s node identified by \"%s\"", $type, print_r($ref, true))); + } + } + return $result; + } } diff --git a/lib/Cake/Model/Aco.php b/lib/Cake/Model/Aco.php index 2a20d8e5..5b9513f5 100755 --- a/lib/Cake/Model/Aco.php +++ b/lib/Cake/Model/Aco.php @@ -21,19 +21,20 @@ * * @package Cake.Model */ -class Aco extends AclNode { +class Aco extends AclNode +{ -/** - * Model name - * - * @var string - */ - public $name = 'Aco'; + /** + * Model name + * + * @var string + */ + public $name = 'Aco'; -/** - * Binds to ARO nodes through permissions settings - * - * @var array - */ - public $hasAndBelongsToMany = array('Aro' => array('with' => 'Permission')); + /** + * Binds to ARO nodes through permissions settings + * + * @var array + */ + public $hasAndBelongsToMany = ['Aro' => ['with' => 'Permission']]; } diff --git a/lib/Cake/Model/AcoAction.php b/lib/Cake/Model/AcoAction.php index 20cfe279..fd15f9a2 100755 --- a/lib/Cake/Model/AcoAction.php +++ b/lib/Cake/Model/AcoAction.php @@ -21,19 +21,20 @@ * * @package Cake.Model */ -class AcoAction extends AppModel { +class AcoAction extends AppModel +{ -/** - * Model name - * - * @var string - */ - public $name = 'AcoAction'; + /** + * Model name + * + * @var string + */ + public $name = 'AcoAction'; -/** - * ACO Actions belong to ACOs - * - * @var array - */ - public $belongsTo = array('Aco'); + /** + * ACO Actions belong to ACOs + * + * @var array + */ + public $belongsTo = ['Aco']; } diff --git a/lib/Cake/Model/Aro.php b/lib/Cake/Model/Aro.php index de8e9fbf..0f76fb65 100755 --- a/lib/Cake/Model/Aro.php +++ b/lib/Cake/Model/Aro.php @@ -21,19 +21,20 @@ * * @package Cake.Model */ -class Aro extends AclNode { +class Aro extends AclNode +{ -/** - * Model name - * - * @var string - */ - public $name = 'Aro'; + /** + * Model name + * + * @var string + */ + public $name = 'Aro'; -/** - * AROs are linked to ACOs by means of Permission - * - * @var array - */ - public $hasAndBelongsToMany = array('Aco' => array('with' => 'Permission')); + /** + * AROs are linked to ACOs by means of Permission + * + * @var array + */ + public $hasAndBelongsToMany = ['Aco' => ['with' => 'Permission']]; } diff --git a/lib/Cake/Model/Behavior/AclBehavior.php b/lib/Cake/Model/Behavior/AclBehavior.php index 45a32609..aee75f35 100755 --- a/lib/Cake/Model/Behavior/AclBehavior.php +++ b/lib/Cake/Model/Behavior/AclBehavior.php @@ -30,115 +30,120 @@ * @package Cake.Model.Behavior * @link https://book.cakephp.org/2.0/en/core-libraries/behaviors/acl.html */ -class AclBehavior extends ModelBehavior { +class AclBehavior extends ModelBehavior +{ -/** - * Maps ACL type options to ACL models - * - * @var array - */ - protected $_typeMaps = array('requester' => 'Aro', 'controlled' => 'Aco', 'both' => array('Aro', 'Aco')); + /** + * Maps ACL type options to ACL models + * + * @var array + */ + protected $_typeMaps = ['requester' => 'Aro', 'controlled' => 'Aco', 'both' => ['Aro', 'Aco']]; -/** - * Sets up the configuration for the model, and loads ACL models if they haven't been already - * - * @param Model $model Model using this behavior. - * @param array $config Configuration options. - * @return void - */ - public function setup(Model $model, $config = array()) { - if (isset($config[0])) { - $config['type'] = $config[0]; - unset($config[0]); - } - $this->settings[$model->name] = array_merge(array('type' => 'controlled'), $config); - $this->settings[$model->name]['type'] = strtolower($this->settings[$model->name]['type']); + /** + * Sets up the configuration for the model, and loads ACL models if they haven't been already + * + * @param Model $model Model using this behavior. + * @param array $config Configuration options. + * @return void + */ + public function setup(Model $model, $config = []) + { + if (isset($config[0])) { + $config['type'] = $config[0]; + unset($config[0]); + } + $this->settings[$model->name] = array_merge(['type' => 'controlled'], $config); + $this->settings[$model->name]['type'] = strtolower($this->settings[$model->name]['type']); - $types = $this->_typeMaps[$this->settings[$model->name]['type']]; + $types = $this->_typeMaps[$this->settings[$model->name]['type']]; - if (!is_array($types)) { - $types = array($types); - } - foreach ($types as $type) { - $model->{$type} = ClassRegistry::init($type); - } - if (!method_exists($model, 'parentNode')) { - trigger_error(__d('cake_dev', 'Callback %s not defined in %s', 'parentNode()', $model->alias), E_USER_WARNING); - } - } + if (!is_array($types)) { + $types = [$types]; + } + foreach ($types as $type) { + $model->{$type} = ClassRegistry::init($type); + } + if (!method_exists($model, 'parentNode')) { + trigger_error(__d('cake_dev', 'Callback %s not defined in %s', 'parentNode()', $model->alias), E_USER_WARNING); + } + } -/** - * Retrieves the Aro/Aco node for this model - * - * @param Model $model Model using this behavior. - * @param string|array|Model $ref Array with 'model' and 'foreign_key', model object, or string value - * @param string $type Only needed when Acl is set up as 'both', specify 'Aro' or 'Aco' to get the correct node - * @return array - * @link https://book.cakephp.org/2.0/en/core-libraries/behaviors/acl.html#node - */ - public function node(Model $model, $ref = null, $type = null) { - if (empty($type)) { - $type = $this->_typeMaps[$this->settings[$model->name]['type']]; - if (is_array($type)) { - trigger_error(__d('cake_dev', 'AclBehavior is setup with more then one type, please specify type parameter for node()'), E_USER_WARNING); - return array(); - } - } - if (empty($ref)) { - $ref = array('model' => $model->name, 'foreign_key' => $model->id); - } - return $model->{$type}->node($ref); - } + /** + * Creates a new ARO/ACO node bound to this record + * + * @param Model $model Model using this behavior. + * @param bool $created True if this is a new record + * @param array $options Options passed from Model::save(). + * @return void + */ + public function afterSave(Model $model, $created, $options = []) + { + $types = $this->_typeMaps[$this->settings[$model->name]['type']]; + if (!is_array($types)) { + $types = [$types]; + } + foreach ($types as $type) { + $parent = $model->parentNode($type); + if (!empty($parent)) { + $parent = $this->node($model, $parent, $type); + } + $data = [ + 'parent_id' => isset($parent[0][$type]['id']) ? $parent[0][$type]['id'] : null, + 'model' => $model->name, + 'foreign_key' => $model->id + ]; + if (!$created) { + $node = $this->node($model, null, $type); + $data['id'] = isset($node[0][$type]['id']) ? $node[0][$type]['id'] : null; + } + $model->{$type}->create(); + $model->{$type}->save($data); + } + } -/** - * Creates a new ARO/ACO node bound to this record - * - * @param Model $model Model using this behavior. - * @param bool $created True if this is a new record - * @param array $options Options passed from Model::save(). - * @return void - */ - public function afterSave(Model $model, $created, $options = array()) { - $types = $this->_typeMaps[$this->settings[$model->name]['type']]; - if (!is_array($types)) { - $types = array($types); - } - foreach ($types as $type) { - $parent = $model->parentNode($type); - if (!empty($parent)) { - $parent = $this->node($model, $parent, $type); - } - $data = array( - 'parent_id' => isset($parent[0][$type]['id']) ? $parent[0][$type]['id'] : null, - 'model' => $model->name, - 'foreign_key' => $model->id - ); - if (!$created) { - $node = $this->node($model, null, $type); - $data['id'] = isset($node[0][$type]['id']) ? $node[0][$type]['id'] : null; - } - $model->{$type}->create(); - $model->{$type}->save($data); - } - } + /** + * Retrieves the Aro/Aco node for this model + * + * @param Model $model Model using this behavior. + * @param string|array|Model $ref Array with 'model' and 'foreign_key', model object, or string value + * @param string $type Only needed when Acl is set up as 'both', specify 'Aro' or 'Aco' to get the correct node + * @return array + * @link https://book.cakephp.org/2.0/en/core-libraries/behaviors/acl.html#node + */ + public function node(Model $model, $ref = null, $type = null) + { + if (empty($type)) { + $type = $this->_typeMaps[$this->settings[$model->name]['type']]; + if (is_array($type)) { + trigger_error(__d('cake_dev', 'AclBehavior is setup with more then one type, please specify type parameter for node()'), E_USER_WARNING); + return []; + } + } + if (empty($ref)) { + $ref = ['model' => $model->name, 'foreign_key' => $model->id]; + } + return $model->{$type}->node($ref); + } -/** - * Destroys the ARO/ACO node bound to the deleted record - * - * @param Model $model Model using this behavior. - * @return void - */ - public function afterDelete(Model $model) { - $types = $this->_typeMaps[$this->settings[$model->name]['type']]; - if (!is_array($types)) { - $types = array($types); - } - foreach ($types as $type) { - $node = Hash::extract($this->node($model, null, $type), "0.{$type}.id"); - if (!empty($node)) { - $model->{$type}->delete($node); - } - } - } + /** + * Destroys the ARO/ACO node bound to the deleted record + * + * @param Model $model Model using this behavior. + * @return void + */ + public function afterDelete(Model $model) + { + $types = $this->_typeMaps[$this->settings[$model->name]['type']]; + if (!is_array($types)) { + $types = [$types]; + } + foreach ($types as $type) { + $node = Hash::extract($this->node($model, null, $type), "0.{$type}.id"); + if (!empty($node)) { + $model->{$type}->delete($node); + } + } + } } diff --git a/lib/Cake/Model/Behavior/ContainableBehavior.php b/lib/Cake/Model/Behavior/ContainableBehavior.php index a494dd7c..0b315e95 100755 --- a/lib/Cake/Model/Behavior/ContainableBehavior.php +++ b/lib/Cake/Model/Behavior/ContainableBehavior.php @@ -28,404 +28,412 @@ * @package Cake.Model.Behavior * @link https://book.cakephp.org/2.0/en/core-libraries/behaviors/containable.html */ -class ContainableBehavior extends ModelBehavior { +class ContainableBehavior extends ModelBehavior +{ -/** - * Types of relationships available for models - * - * @var array - */ - public $types = array('belongsTo', 'hasOne', 'hasMany', 'hasAndBelongsToMany'); + /** + * Types of relationships available for models + * + * @var array + */ + public $types = ['belongsTo', 'hasOne', 'hasMany', 'hasAndBelongsToMany']; -/** - * Runtime configuration for this behavior - * - * @var array - */ - public $runtime = array(); + /** + * Runtime configuration for this behavior + * + * @var array + */ + public $runtime = []; -/** - * Initiate behavior for the model using specified settings. - * - * Available settings: - * - * - recursive: (boolean, optional) set to true to allow containable to automatically - * determine the recursiveness level needed to fetch specified models, - * and set the model recursiveness to this level. setting it to false - * disables this feature. DEFAULTS TO: true - * - notices: (boolean, optional) issues E_NOTICES for bindings referenced in a - * containable call that are not valid. DEFAULTS TO: true - * - autoFields: (boolean, optional) auto-add needed fields to fetch requested - * bindings. DEFAULTS TO: true - * - * @param Model $Model Model using the behavior - * @param array $settings Settings to override for model. - * @return void - */ - public function setup(Model $Model, $settings = array()) { - if (!isset($this->settings[$Model->alias])) { - $this->settings[$Model->alias] = array('recursive' => true, 'notices' => true, 'autoFields' => true); - } - $this->settings[$Model->alias] = array_merge($this->settings[$Model->alias], $settings); - } + /** + * Initiate behavior for the model using specified settings. + * + * Available settings: + * + * - recursive: (boolean, optional) set to true to allow containable to automatically + * determine the recursiveness level needed to fetch specified models, + * and set the model recursiveness to this level. setting it to false + * disables this feature. DEFAULTS TO: true + * - notices: (boolean, optional) issues E_NOTICES for bindings referenced in a + * containable call that are not valid. DEFAULTS TO: true + * - autoFields: (boolean, optional) auto-add needed fields to fetch requested + * bindings. DEFAULTS TO: true + * + * @param Model $Model Model using the behavior + * @param array $settings Settings to override for model. + * @return void + */ + public function setup(Model $Model, $settings = []) + { + if (!isset($this->settings[$Model->alias])) { + $this->settings[$Model->alias] = ['recursive' => true, 'notices' => true, 'autoFields' => true]; + } + $this->settings[$Model->alias] = array_merge($this->settings[$Model->alias], $settings); + } -/** - * Runs before a find() operation. Used to allow 'contain' setting - * as part of the find call, like this: - * - * `Model->find('all', array('contain' => array('Model1', 'Model2')));` - * - * ``` - * Model->find('all', array('contain' => array( - * 'Model1' => array('Model11', 'Model12'), - * 'Model2', - * 'Model3' => array( - * 'Model31' => 'Model311', - * 'Model32', - * 'Model33' => array('Model331', 'Model332') - * ))); - * ``` - * - * @param Model $Model Model using the behavior - * @param array $query Query parameters as set by cake - * @return array - */ - public function beforeFind(Model $Model, $query) { - $reset = (isset($query['reset']) ? $query['reset'] : true); - $noContain = false; - $contain = array(); + /** + * Runs before a find() operation. Used to allow 'contain' setting + * as part of the find call, like this: + * + * `Model->find('all', array('contain' => array('Model1', 'Model2')));` + * + * ``` + * Model->find('all', array('contain' => array( + * 'Model1' => array('Model11', 'Model12'), + * 'Model2', + * 'Model3' => array( + * 'Model31' => 'Model311', + * 'Model32', + * 'Model33' => array('Model331', 'Model332') + * ))); + * ``` + * + * @param Model $Model Model using the behavior + * @param array $query Query parameters as set by cake + * @return array + */ + public function beforeFind(Model $Model, $query) + { + $reset = (isset($query['reset']) ? $query['reset'] : true); + $noContain = false; + $contain = []; - if (isset($this->runtime[$Model->alias]['contain'])) { - $noContain = empty($this->runtime[$Model->alias]['contain']); - $contain = $this->runtime[$Model->alias]['contain']; - unset($this->runtime[$Model->alias]['contain']); - } + if (isset($this->runtime[$Model->alias]['contain'])) { + $noContain = empty($this->runtime[$Model->alias]['contain']); + $contain = $this->runtime[$Model->alias]['contain']; + unset($this->runtime[$Model->alias]['contain']); + } - if (isset($query['contain'])) { - $noContain = $noContain || empty($query['contain']); - if ($query['contain'] !== false) { - $contain = array_merge($contain, (array)$query['contain']); - } - } - $noContain = $noContain && empty($contain); + if (isset($query['contain'])) { + $noContain = $noContain || empty($query['contain']); + if ($query['contain'] !== false) { + $contain = array_merge($contain, (array)$query['contain']); + } + } + $noContain = $noContain && empty($contain); - if ($noContain || empty($contain)) { - if ($noContain) { - $query['recursive'] = -1; - } - return $query; - } - if ((isset($contain[0]) && is_bool($contain[0])) || is_bool(end($contain))) { - $reset = is_bool(end($contain)) - ? array_pop($contain) - : array_shift($contain); - } - $containments = $this->containments($Model, $contain); - $map = $this->containmentsMap($containments); + if ($noContain || empty($contain)) { + if ($noContain) { + $query['recursive'] = -1; + } + return $query; + } + if ((isset($contain[0]) && is_bool($contain[0])) || is_bool(end($contain))) { + $reset = is_bool(end($contain)) + ? array_pop($contain) + : array_shift($contain); + } + $containments = $this->containments($Model, $contain); + $map = $this->containmentsMap($containments); - $mandatory = array(); - foreach ($containments['models'] as $model) { - $instance = $model['instance']; - $needed = $this->fieldDependencies($instance, $map, false); - if (!empty($needed)) { - $mandatory = array_merge($mandatory, $needed); - } - if ($contain) { - $backupBindings = array(); - foreach ($this->types as $relation) { - if (!empty($instance->__backAssociation[$relation])) { - $backupBindings[$relation] = $instance->__backAssociation[$relation]; - } else { - $backupBindings[$relation] = $instance->{$relation}; - } - } - foreach ($this->types as $type) { - $unbind = array(); - foreach ($instance->{$type} as $assoc => $options) { - if (!isset($model['keep'][$assoc])) { - $unbind[] = $assoc; - } - } - if (!empty($unbind)) { - if (!$reset && empty($instance->__backOriginalAssociation)) { - $instance->__backOriginalAssociation = $backupBindings; - } - $instance->unbindModel(array($type => $unbind), $reset); - } - foreach ($instance->{$type} as $assoc => $options) { - if (isset($model['keep'][$assoc]) && !empty($model['keep'][$assoc])) { - if (isset($model['keep'][$assoc]['fields'])) { - $model['keep'][$assoc]['fields'] = $this->fieldDependencies($containments['models'][$assoc]['instance'], $map, $model['keep'][$assoc]['fields']); - } - if (!$reset && empty($instance->__backOriginalAssociation)) { - $instance->__backOriginalAssociation = $backupBindings; - } elseif ($reset) { - $instance->__backAssociation[$type] = $backupBindings[$type]; - } - $instance->{$type}[$assoc] = array_merge($instance->{$type}[$assoc], $model['keep'][$assoc]); - } - if (!$reset) { - $instance->__backInnerAssociation[] = $assoc; - } - } - } - } - } + $mandatory = []; + foreach ($containments['models'] as $model) { + $instance = $model['instance']; + $needed = $this->fieldDependencies($instance, $map, false); + if (!empty($needed)) { + $mandatory = array_merge($mandatory, $needed); + } + if ($contain) { + $backupBindings = []; + foreach ($this->types as $relation) { + if (!empty($instance->__backAssociation[$relation])) { + $backupBindings[$relation] = $instance->__backAssociation[$relation]; + } else { + $backupBindings[$relation] = $instance->{$relation}; + } + } + foreach ($this->types as $type) { + $unbind = []; + foreach ($instance->{$type} as $assoc => $options) { + if (!isset($model['keep'][$assoc])) { + $unbind[] = $assoc; + } + } + if (!empty($unbind)) { + if (!$reset && empty($instance->__backOriginalAssociation)) { + $instance->__backOriginalAssociation = $backupBindings; + } + $instance->unbindModel([$type => $unbind], $reset); + } + foreach ($instance->{$type} as $assoc => $options) { + if (isset($model['keep'][$assoc]) && !empty($model['keep'][$assoc])) { + if (isset($model['keep'][$assoc]['fields'])) { + $model['keep'][$assoc]['fields'] = $this->fieldDependencies($containments['models'][$assoc]['instance'], $map, $model['keep'][$assoc]['fields']); + } + if (!$reset && empty($instance->__backOriginalAssociation)) { + $instance->__backOriginalAssociation = $backupBindings; + } else if ($reset) { + $instance->__backAssociation[$type] = $backupBindings[$type]; + } + $instance->{$type}[$assoc] = array_merge($instance->{$type}[$assoc], $model['keep'][$assoc]); + } + if (!$reset) { + $instance->__backInnerAssociation[] = $assoc; + } + } + } + } + } - if ($this->settings[$Model->alias]['recursive']) { - $query['recursive'] = (isset($query['recursive'])) ? max($query['recursive'], $containments['depth']) : $containments['depth']; - } + if ($this->settings[$Model->alias]['recursive']) { + $query['recursive'] = (isset($query['recursive'])) ? max($query['recursive'], $containments['depth']) : $containments['depth']; + } - $autoFields = ($this->settings[$Model->alias]['autoFields'] - && !in_array($Model->findQueryType, array('list', 'count')) - && !empty($query['fields'])); + $autoFields = ($this->settings[$Model->alias]['autoFields'] + && !in_array($Model->findQueryType, ['list', 'count']) + && !empty($query['fields'])); - if (!$autoFields) { - return $query; - } + if (!$autoFields) { + return $query; + } - $query['fields'] = (array)$query['fields']; - foreach (array('hasOne', 'belongsTo') as $type) { - if (!empty($Model->{$type})) { - foreach ($Model->{$type} as $assoc => $data) { - if ($Model->useDbConfig === $Model->{$assoc}->useDbConfig && !empty($data['fields'])) { - foreach ((array)$data['fields'] as $field) { - $query['fields'][] = (strpos($field, '.') === false ? $assoc . '.' : '') . $field; - } - } - } - } - } + $query['fields'] = (array)$query['fields']; + foreach (['hasOne', 'belongsTo'] as $type) { + if (!empty($Model->{$type})) { + foreach ($Model->{$type} as $assoc => $data) { + if ($Model->useDbConfig === $Model->{$assoc}->useDbConfig && !empty($data['fields'])) { + foreach ((array)$data['fields'] as $field) { + $query['fields'][] = (strpos($field, '.') === false ? $assoc . '.' : '') . $field; + } + } + } + } + } - if (!empty($mandatory[$Model->alias])) { - foreach ($mandatory[$Model->alias] as $field) { - if ($field === '--primaryKey--') { - $field = $Model->primaryKey; - } elseif (preg_match('/^.+\.\-\-[^-]+\-\-$/', $field)) { - list($modelName, $field) = explode('.', $field); - if ($Model->useDbConfig === $Model->{$modelName}->useDbConfig) { - $field = $modelName . '.' . ( - ($field === '--primaryKey--') ? $Model->$modelName->primaryKey : $field - ); - } else { - $field = null; - } - } - if ($field !== null) { - $query['fields'][] = $field; - } - } - } - $query['fields'] = array_unique($query['fields']); - return $query; - } + if (!empty($mandatory[$Model->alias])) { + foreach ($mandatory[$Model->alias] as $field) { + if ($field === '--primaryKey--') { + $field = $Model->primaryKey; + } else if (preg_match('/^.+\.\-\-[^-]+\-\-$/', $field)) { + list($modelName, $field) = explode('.', $field); + if ($Model->useDbConfig === $Model->{$modelName}->useDbConfig) { + $field = $modelName . '.' . ( + ($field === '--primaryKey--') ? $Model->$modelName->primaryKey : $field + ); + } else { + $field = null; + } + } + if ($field !== null) { + $query['fields'][] = $field; + } + } + } + $query['fields'] = array_unique($query['fields']); + return $query; + } -/** - * Unbinds all relations from a model except the specified ones. Calling this function without - * parameters unbinds all related models. - * - * @param Model $Model Model on which binding restriction is being applied - * @return void - * @link https://book.cakephp.org/2.0/en/core-libraries/behaviors/containable.html#using-containable - */ - public function contain(Model $Model) { - $args = func_get_args(); - $contain = call_user_func_array('am', array_slice($args, 1)); - $this->runtime[$Model->alias]['contain'] = $contain; - } + /** + * Process containments for model. + * + * @param Model $Model Model on which binding restriction is being applied + * @param array $contain Parameters to use for restricting this model + * @param array $containments Current set of containments + * @param bool $throwErrors Whether non-existent bindings show throw errors + * @return array Containments + */ + public function containments(Model $Model, $contain, $containments = [], $throwErrors = null) + { + $options = ['className', 'joinTable', 'with', 'foreignKey', 'associationForeignKey', 'conditions', 'fields', 'order', 'limit', 'offset', 'unique', 'finderQuery']; + $keep = []; + if ($throwErrors === null) { + $throwErrors = (empty($this->settings[$Model->alias]) ? true : $this->settings[$Model->alias]['notices']); + } + foreach ((array)$contain as $name => $children) { + if (is_numeric($name)) { + $name = $children; + $children = []; + } + if (preg_match('/(? $children]; + } -/** - * Permanently restore the original binding settings of given model, useful - * for restoring the bindings after using 'reset' => false as part of the - * contain call. - * - * @param Model $Model Model on which to reset bindings - * @return void - */ - public function resetBindings(Model $Model) { - if (!empty($Model->__backOriginalAssociation)) { - $Model->__backAssociation = $Model->__backOriginalAssociation; - unset($Model->__backOriginalAssociation); - } - $Model->resetAssociations(); - if (!empty($Model->__backInnerAssociation)) { - $assocs = $Model->__backInnerAssociation; - $Model->__backInnerAssociation = array(); - foreach ($assocs as $currentModel) { - $this->resetBindings($Model->$currentModel); - } - } - } + $children = (array)$children; + foreach ($children as $key => $val) { + if (is_string($key) && is_string($val) && !in_array($key, $options, true)) { + $children[$key] = (array)$val; + } + } -/** - * Process containments for model. - * - * @param Model $Model Model on which binding restriction is being applied - * @param array $contain Parameters to use for restricting this model - * @param array $containments Current set of containments - * @param bool $throwErrors Whether non-existent bindings show throw errors - * @return array Containments - */ - public function containments(Model $Model, $contain, $containments = array(), $throwErrors = null) { - $options = array('className', 'joinTable', 'with', 'foreignKey', 'associationForeignKey', 'conditions', 'fields', 'order', 'limit', 'offset', 'unique', 'finderQuery'); - $keep = array(); - if ($throwErrors === null) { - $throwErrors = (empty($this->settings[$Model->alias]) ? true : $this->settings[$Model->alias]['notices']); - } - foreach ((array)$contain as $name => $children) { - if (is_numeric($name)) { - $name = $children; - $children = array(); - } - if (preg_match('/(? $children); - } + $keys = array_keys($children); + if ($keys && isset($children[0])) { + $keys = array_merge(array_values($children), $keys); + } - $children = (array)$children; - foreach ($children as $key => $val) { - if (is_string($key) && is_string($val) && !in_array($key, $options, true)) { - $children[$key] = (array)$val; - } - } + foreach ($keys as $i => $key) { + if (is_array($key)) { + continue; + } + $optionKey = in_array($key, $options, true); + if (!$optionKey && is_string($key) && preg_match('/^[a-z(]/', $key) && (!isset($Model->{$key}) || !is_object($Model->{$key}))) { + $option = 'fields'; + $val = [$key]; + if ($key{0} === '(') { + $val = preg_split('/\s*,\s*/', substr($key, 1, -1)); + } else if (preg_match('/ASC|DESC$/', $key)) { + $option = 'order'; + $val = $Model->{$name}->alias . '.' . $key; + } else if (preg_match('/[ =!]/', $key)) { + $option = 'conditions'; + $val = $Model->{$name}->alias . '.' . $key; + } + $children[$option] = is_array($val) ? $val : [$val]; + $newChildren = null; + if (!empty($name) && !empty($children[$key])) { + $newChildren = $children[$key]; + } + unset($children[$key], $children[$i]); + $key = $option; + $optionKey = true; + if (!empty($newChildren)) { + $children = Hash::merge($children, $newChildren); + } + } + if ($optionKey && isset($children[$key])) { + if (!empty($keep[$name][$key]) && is_array($keep[$name][$key])) { + $keep[$name][$key] = array_merge((isset($keep[$name][$key]) ? $keep[$name][$key] : []), (array)$children[$key]); + } else { + $keep[$name][$key] = $children[$key]; + } + unset($children[$key]); + } + } - $keys = array_keys($children); - if ($keys && isset($children[0])) { - $keys = array_merge(array_values($children), $keys); - } + if (!isset($Model->{$name}) || !is_object($Model->{$name})) { + if ($throwErrors) { + trigger_error(__d('cake_dev', 'Model "%s" is not associated with model "%s"', $Model->alias, $name), E_USER_WARNING); + } + continue; + } - foreach ($keys as $i => $key) { - if (is_array($key)) { - continue; - } - $optionKey = in_array($key, $options, true); - if (!$optionKey && is_string($key) && preg_match('/^[a-z(]/', $key) && (!isset($Model->{$key}) || !is_object($Model->{$key}))) { - $option = 'fields'; - $val = array($key); - if ($key{0} === '(') { - $val = preg_split('/\s*,\s*/', substr($key, 1, -1)); - } elseif (preg_match('/ASC|DESC$/', $key)) { - $option = 'order'; - $val = $Model->{$name}->alias . '.' . $key; - } elseif (preg_match('/[ =!]/', $key)) { - $option = 'conditions'; - $val = $Model->{$name}->alias . '.' . $key; - } - $children[$option] = is_array($val) ? $val : array($val); - $newChildren = null; - if (!empty($name) && !empty($children[$key])) { - $newChildren = $children[$key]; - } - unset($children[$key], $children[$i]); - $key = $option; - $optionKey = true; - if (!empty($newChildren)) { - $children = Hash::merge($children, $newChildren); - } - } - if ($optionKey && isset($children[$key])) { - if (!empty($keep[$name][$key]) && is_array($keep[$name][$key])) { - $keep[$name][$key] = array_merge((isset($keep[$name][$key]) ? $keep[$name][$key] : array()), (array)$children[$key]); - } else { - $keep[$name][$key] = $children[$key]; - } - unset($children[$key]); - } - } + $containments = $this->containments($Model->{$name}, $children, $containments); + $depths[] = $containments['depth'] + 1; + if (!isset($keep[$name])) { + $keep[$name] = []; + } + } - if (!isset($Model->{$name}) || !is_object($Model->{$name})) { - if ($throwErrors) { - trigger_error(__d('cake_dev', 'Model "%s" is not associated with model "%s"', $Model->alias, $name), E_USER_WARNING); - } - continue; - } + if (!isset($containments['models'][$Model->alias])) { + $containments['models'][$Model->alias] = ['keep' => [], 'instance' => &$Model]; + } - $containments = $this->containments($Model->{$name}, $children, $containments); - $depths[] = $containments['depth'] + 1; - if (!isset($keep[$name])) { - $keep[$name] = array(); - } - } + $containments['models'][$Model->alias]['keep'] = array_merge($containments['models'][$Model->alias]['keep'], $keep); + $containments['depth'] = empty($depths) ? 0 : max($depths); + return $containments; + } - if (!isset($containments['models'][$Model->alias])) { - $containments['models'][$Model->alias] = array('keep' => array(), 'instance' => &$Model); - } + /** + * Build the map of containments + * + * @param array $containments Containments + * @return array Built containments + */ + public function containmentsMap($containments) + { + $map = []; + foreach ($containments['models'] as $name => $model) { + $instance = $model['instance']; + foreach ($this->types as $type) { + foreach ($instance->{$type} as $assoc => $options) { + if (isset($model['keep'][$assoc])) { + $map[$name][$type] = isset($map[$name][$type]) ? array_merge($map[$name][$type], (array)$assoc) : (array)$assoc; + } + } + } + } + return $map; + } - $containments['models'][$Model->alias]['keep'] = array_merge($containments['models'][$Model->alias]['keep'], $keep); - $containments['depth'] = empty($depths) ? 0 : max($depths); - return $containments; - } + /** + * Calculate needed fields to fetch the required bindings for the given model. + * + * @param Model $Model Model + * @param array $map Map of relations for given model + * @param array|bool $fields If array, fields to initially load, if false use $Model as primary model + * @return array Fields + */ + public function fieldDependencies(Model $Model, $map, $fields = []) + { + if ($fields === false) { + foreach ($map as $parent => $children) { + foreach ($children as $type => $bindings) { + foreach ($bindings as $dependency) { + if ($type === 'hasAndBelongsToMany') { + $fields[$parent][] = '--primaryKey--'; + } else if ($type === 'belongsTo') { + $fields[$parent][] = $dependency . '.--primaryKey--'; + } + } + } + } + return $fields; + } + if (empty($map[$Model->alias])) { + return $fields; + } + foreach ($map[$Model->alias] as $type => $bindings) { + foreach ($bindings as $dependency) { + $innerFields = []; + switch ($type) { + case 'belongsTo': + $fields[] = $Model->{$type}[$dependency]['foreignKey']; + break; + case 'hasOne': + case 'hasMany': + $innerFields[] = $Model->$dependency->primaryKey; + $fields[] = $Model->primaryKey; + break; + } + if (!empty($innerFields) && !empty($Model->{$type}[$dependency]['fields'])) { + $Model->{$type}[$dependency]['fields'] = array_unique(array_merge($Model->{$type}[$dependency]['fields'], $innerFields)); + } + } + } + return array_unique($fields); + } -/** - * Calculate needed fields to fetch the required bindings for the given model. - * - * @param Model $Model Model - * @param array $map Map of relations for given model - * @param array|bool $fields If array, fields to initially load, if false use $Model as primary model - * @return array Fields - */ - public function fieldDependencies(Model $Model, $map, $fields = array()) { - if ($fields === false) { - foreach ($map as $parent => $children) { - foreach ($children as $type => $bindings) { - foreach ($bindings as $dependency) { - if ($type === 'hasAndBelongsToMany') { - $fields[$parent][] = '--primaryKey--'; - } elseif ($type === 'belongsTo') { - $fields[$parent][] = $dependency . '.--primaryKey--'; - } - } - } - } - return $fields; - } - if (empty($map[$Model->alias])) { - return $fields; - } - foreach ($map[$Model->alias] as $type => $bindings) { - foreach ($bindings as $dependency) { - $innerFields = array(); - switch ($type) { - case 'belongsTo': - $fields[] = $Model->{$type}[$dependency]['foreignKey']; - break; - case 'hasOne': - case 'hasMany': - $innerFields[] = $Model->$dependency->primaryKey; - $fields[] = $Model->primaryKey; - break; - } - if (!empty($innerFields) && !empty($Model->{$type}[$dependency]['fields'])) { - $Model->{$type}[$dependency]['fields'] = array_unique(array_merge($Model->{$type}[$dependency]['fields'], $innerFields)); - } - } - } - return array_unique($fields); - } + /** + * Unbinds all relations from a model except the specified ones. Calling this function without + * parameters unbinds all related models. + * + * @param Model $Model Model on which binding restriction is being applied + * @return void + * @link https://book.cakephp.org/2.0/en/core-libraries/behaviors/containable.html#using-containable + */ + public function contain(Model $Model) + { + $args = func_get_args(); + $contain = call_user_func_array('am', array_slice($args, 1)); + $this->runtime[$Model->alias]['contain'] = $contain; + } -/** - * Build the map of containments - * - * @param array $containments Containments - * @return array Built containments - */ - public function containmentsMap($containments) { - $map = array(); - foreach ($containments['models'] as $name => $model) { - $instance = $model['instance']; - foreach ($this->types as $type) { - foreach ($instance->{$type} as $assoc => $options) { - if (isset($model['keep'][$assoc])) { - $map[$name][$type] = isset($map[$name][$type]) ? array_merge($map[$name][$type], (array)$assoc) : (array)$assoc; - } - } - } - } - return $map; - } + /** + * Permanently restore the original binding settings of given model, useful + * for restoring the bindings after using 'reset' => false as part of the + * contain call. + * + * @param Model $Model Model on which to reset bindings + * @return void + */ + public function resetBindings(Model $Model) + { + if (!empty($Model->__backOriginalAssociation)) { + $Model->__backAssociation = $Model->__backOriginalAssociation; + unset($Model->__backOriginalAssociation); + } + $Model->resetAssociations(); + if (!empty($Model->__backInnerAssociation)) { + $assocs = $Model->__backInnerAssociation; + $Model->__backInnerAssociation = []; + foreach ($assocs as $currentModel) { + $this->resetBindings($Model->$currentModel); + } + } + } } diff --git a/lib/Cake/Model/Behavior/TranslateBehavior.php b/lib/Cake/Model/Behavior/TranslateBehavior.php index e4bb5373..afd18bac 100755 --- a/lib/Cake/Model/Behavior/TranslateBehavior.php +++ b/lib/Cake/Model/Behavior/TranslateBehavior.php @@ -24,754 +24,776 @@ * @package Cake.Model.Behavior * @link https://book.cakephp.org/2.0/en/core-libraries/behaviors/translate.html */ -class TranslateBehavior extends ModelBehavior { - -/** - * Used for runtime configuration of model - * - * @var array - */ - public $runtime = array(); - -/** - * Stores the joinTable object for generating joins. - * - * @var object - */ - protected $_joinTable; - -/** - * Stores the runtime model for generating joins. - * - * @var Model - */ - protected $_runtimeModel; - -/** - * Callback - * - * $config for TranslateBehavior should be - * array('fields' => array('field_one', - * 'field_two' => 'FieldAssoc', 'field_three')) - * - * With above example only one permanent hasMany will be joined (for field_two - * as FieldAssoc) - * - * $config could be empty - and translations configured dynamically by - * bindTranslation() method - * - * By default INNER joins are used to fetch translations. In order to use - * other join types $config should contain 'joinType' key: - * ``` - * array( - * 'fields' => array('field_one', 'field_two' => 'FieldAssoc', 'field_three'), - * 'joinType' => 'LEFT', - * ) - * ``` - * In a model it may be configured this way: - * ``` - * public $actsAs = array( - * 'Translate' => array( - * 'content', - * 'title', - * 'joinType' => 'LEFT', - * ), - * ); - * ``` - * - * @param Model $Model Model the behavior is being attached to. - * @param array $config Array of configuration information. - * @return mixed - */ - public function setup(Model $Model, $config = array()) { - $db = ConnectionManager::getDataSource($Model->useDbConfig); - if (!$db->connected) { - trigger_error( - __d('cake_dev', 'Datasource %s for TranslateBehavior of model %s is not connected', $Model->useDbConfig, $Model->alias), - E_USER_ERROR - ); - return false; - } - - $this->settings[$Model->alias] = array(); - $this->runtime[$Model->alias] = array( - 'fields' => array(), - 'joinType' => 'INNER', - ); - if (isset($config['joinType'])) { - $this->runtime[$Model->alias]['joinType'] = $config['joinType']; - unset($config['joinType']); - } - $this->translateModel($Model); - return $this->bindTranslation($Model, $config, false); - } - -/** - * Cleanup Callback unbinds bound translations and deletes setting information. - * - * @param Model $Model Model being detached. - * @return void - */ - public function cleanup(Model $Model) { - $this->unbindTranslation($Model); - unset($this->settings[$Model->alias]); - unset($this->runtime[$Model->alias]); - } - -/** - * beforeFind Callback - * - * @param Model $Model Model find is being run on. - * @param array $query Array of Query parameters. - * @return array Modified query - */ - public function beforeFind(Model $Model, $query) { - $this->runtime[$Model->alias]['virtualFields'] = $Model->virtualFields; - $locale = $this->_getLocale($Model); - if (empty($locale)) { - return $query; - } - $db = $Model->getDataSource(); - $RuntimeModel = $this->translateModel($Model); - - if (!empty($RuntimeModel->tablePrefix)) { - $tablePrefix = $RuntimeModel->tablePrefix; - } else { - $tablePrefix = $db->config['prefix']; - } - $joinTable = new StdClass(); - $joinTable->tablePrefix = $tablePrefix; - $joinTable->table = $RuntimeModel->table; - $joinTable->schemaName = $RuntimeModel->getDataSource()->getSchemaName(); - - $this->_joinTable = $joinTable; - $this->_runtimeModel = $RuntimeModel; - - if (is_string($query['fields'])) { - if ($query['fields'] === "COUNT(*) AS {$db->name('count')}") { - $query['fields'] = "COUNT(DISTINCT({$db->name($Model->escapeField())})) {$db->alias}count"; - $query['joins'][] = array( - 'type' => $this->runtime[$Model->alias]['joinType'], - 'alias' => $RuntimeModel->alias, - 'table' => $joinTable, - 'conditions' => array( - $Model->escapeField() => $db->identifier($RuntimeModel->escapeField('foreign_key')), - $RuntimeModel->escapeField('model') => $Model->name, - $RuntimeModel->escapeField('locale') => $locale - ) - ); - $conditionFields = $this->_checkConditions($Model, $query); - foreach ($conditionFields as $field) { - $query = $this->_addJoin($Model, $query, $field, $field, $locale); - } - unset($this->_joinTable, $this->_runtimeModel); - return $query; - } else { - $query['fields'] = CakeText::tokenize($query['fields']); - } - } - $addFields = $this->_getFields($Model, $query); - $this->runtime[$Model->alias]['virtualFields'] = $Model->virtualFields; - $query = $this->_addAllJoins($Model, $query, $addFields); - $this->runtime[$Model->alias]['beforeFind'] = $addFields; - unset($this->_joinTable, $this->_runtimeModel); - return $query; - } - -/** - * Gets fields to be retrieved. - * - * @param Model $Model The model being worked on. - * @param array $query The query array to take fields from. - * @return array The fields. - */ - protected function _getFields(Model $Model, $query) { - $fields = array_merge( - $this->settings[$Model->alias], - $this->runtime[$Model->alias]['fields'] - ); - $addFields = array(); - if (empty($query['fields'])) { - $addFields = $fields; - } elseif (is_array($query['fields'])) { - $isAllFields = ( - in_array($Model->alias . '.' . '*', $query['fields']) || - in_array($Model->escapeField('*'), $query['fields']) - ); - foreach ($fields as $key => $value) { - $field = (is_numeric($key)) ? $value : $key; - if ($isAllFields || - in_array($Model->alias . '.' . $field, $query['fields']) || - in_array($field, $query['fields']) - ) { - $addFields[] = $field; - } - } - } - return $addFields; - } - -/** - * Appends all necessary joins for translated fields. - * - * @param Model $Model The model being worked on. - * @param array $query The query array to append joins to. - * @param array $addFields The fields being joined. - * @return array The modified query - */ - protected function _addAllJoins(Model $Model, $query, $addFields) { - $locale = $this->_getLocale($Model); - if ($addFields) { - foreach ($addFields as $_f => $field) { - $aliasField = is_numeric($_f) ? $field : $_f; - foreach (array($aliasField, $Model->alias . '.' . $aliasField) as $_field) { - $key = array_search($_field, (array)$query['fields']); - if ($key !== false) { - unset($query['fields'][$key]); - } - } - $query = $this->_addJoin($Model, $query, $field, $aliasField, $locale); - } - } - return $query; - } - -/** - * Check a query's conditions for translated fields. - * Return an array of translated fields found in the conditions. - * - * @param Model $Model The model being read. - * @param array $query The query array. - * @return array The list of translated fields that are in the conditions. - */ - protected function _checkConditions(Model $Model, $query) { - if (empty($query['conditions']) || (!empty($query['conditions']) && !is_array($query['conditions']))) { - return array(); - } - return $this->_getConditionFields($Model, $query['conditions']); - } - -/** - * Extracts condition field names recursively. - * - * @param Model $Model The model being read. - * @param array $conditions The conditions array. - * @return array The list of condition fields. - */ - protected function _getConditionFields(Model $Model, $conditions) { - $conditionFields = array(); - foreach ($conditions as $col => $val) { - if (is_array($val)) { - $subConditionFields = $this->_getConditionFields($Model, $val); - $conditionFields = array_merge($conditionFields, $subConditionFields); - } - foreach ($this->settings[$Model->alias] as $field => $assoc) { - if (is_numeric($field)) { - $field = $assoc; - } - if (strpos($col, $field) !== false) { - $conditionFields[] = $field; - } - } - } - return $conditionFields; - } - -/** - * Appends a join for translated fields. - * - * @param Model $Model The model being worked on. - * @param array $query The query array to append a join to. - * @param string $field The field name being joined. - * @param string $aliasField The aliased field name being joined. - * @param string|array $locale The locale(s) having joins added. - * @return array The modified query - */ - protected function _addJoin(Model $Model, $query, $field, $aliasField, $locale) { - $db = ConnectionManager::getDataSource($Model->useDbConfig); - $RuntimeModel = $this->_runtimeModel; - $joinTable = $this->_joinTable; - $aliasVirtual = "i18n_{$field}"; - $alias = "I18n__{$field}"; - if (is_array($locale)) { - foreach ($locale as $_locale) { - $aliasVirtualLocale = "{$aliasVirtual}_{$_locale}"; - $aliasLocale = "{$alias}__{$_locale}"; - $Model->virtualFields[$aliasVirtualLocale] = "{$aliasLocale}.content"; - if (!empty($query['fields']) && is_array($query['fields'])) { - $query['fields'][] = $aliasVirtualLocale; - } - $query['joins'][] = array( - 'type' => 'LEFT', - 'alias' => $aliasLocale, - 'table' => $joinTable, - 'conditions' => array( - $Model->escapeField() => $db->identifier("{$aliasLocale}.foreign_key"), - "{$aliasLocale}.model" => $Model->name, - "{$aliasLocale}.{$RuntimeModel->displayField}" => $aliasField, - "{$aliasLocale}.locale" => $_locale - ) - ); - } - } else { - $Model->virtualFields[$aliasVirtual] = "{$alias}.content"; - if (!empty($query['fields']) && is_array($query['fields'])) { - $query['fields'][] = $aliasVirtual; - } - $query['joins'][] = array( - 'type' => $this->runtime[$Model->alias]['joinType'], - 'alias' => $alias, - 'table' => $joinTable, - 'conditions' => array( - "{$Model->alias}.{$Model->primaryKey}" => $db->identifier("{$alias}.foreign_key"), - "{$alias}.model" => $Model->name, - "{$alias}.{$RuntimeModel->displayField}" => $aliasField, - "{$alias}.locale" => $locale - ) - ); - } - return $query; - } - -/** - * afterFind Callback - * - * @param Model $Model Model find was run on - * @param array $results Array of model results. - * @param bool $primary Did the find originate on $model. - * @return array Modified results - */ - public function afterFind(Model $Model, $results, $primary = false) { - $Model->virtualFields = $this->runtime[$Model->alias]['virtualFields']; - - $this->runtime[$Model->alias]['virtualFields'] = array(); - if (!empty($this->runtime[$Model->alias]['restoreFields'])) { - $this->runtime[$Model->alias]['fields'] = $this->runtime[$Model->alias]['restoreFields']; - unset($this->runtime[$Model->alias]['restoreFields']); - } - - $locale = $this->_getLocale($Model); - - if (empty($locale) || empty($results) || empty($this->runtime[$Model->alias]['beforeFind'])) { - return $results; - } - $beforeFind = $this->runtime[$Model->alias]['beforeFind']; - - foreach ($results as $key => &$row) { - $results[$key][$Model->alias]['locale'] = (is_array($locale)) ? current($locale) : $locale; - foreach ($beforeFind as $_f => $field) { - $aliasField = is_numeric($_f) ? $field : $_f; - $aliasVirtual = "i18n_{$field}"; - if (is_array($locale)) { - foreach ($locale as $_locale) { - $aliasVirtualLocale = "{$aliasVirtual}_{$_locale}"; - if (!isset($row[$Model->alias][$aliasField]) && !empty($row[$Model->alias][$aliasVirtualLocale])) { - $row[$Model->alias][$aliasField] = $row[$Model->alias][$aliasVirtualLocale]; - $row[$Model->alias]['locale'] = $_locale; - } - unset($row[$Model->alias][$aliasVirtualLocale]); - } - - if (!isset($row[$Model->alias][$aliasField])) { - $row[$Model->alias][$aliasField] = ''; - } - } else { - $value = ''; - if (isset($row[$Model->alias][$aliasVirtual])) { - $value = $row[$Model->alias][$aliasVirtual]; - } - $row[$Model->alias][$aliasField] = $value; - unset($row[$Model->alias][$aliasVirtual]); - } - } - } - return $results; - } - -/** - * beforeValidate Callback - * - * @param Model $Model Model invalidFields was called on. - * @param array $options Options passed from Model::save(). - * @return bool - * @see Model::save() - */ - public function beforeValidate(Model $Model, $options = array()) { - unset($this->runtime[$Model->alias]['beforeSave']); - $this->_setRuntimeData($Model); - return true; - } - -/** - * beforeSave callback. - * - * Copies data into the runtime property when `$options['validate']` is - * disabled. Or the runtime data hasn't been set yet. - * - * @param Model $Model Model save was called on. - * @param array $options Options passed from Model::save(). - * @return bool true. - * @see Model::save() - */ - public function beforeSave(Model $Model, $options = array()) { - if (isset($options['validate']) && !$options['validate']) { - unset($this->runtime[$Model->alias]['beforeSave']); - } - if (isset($this->runtime[$Model->alias]['beforeSave'])) { - return true; - } - $this->_setRuntimeData($Model); - return true; - } - -/** - * Sets the runtime data. - * - * Used from beforeValidate() and beforeSave() for compatibility issues, - * and to allow translations to be persisted even when validation - * is disabled. - * - * @param Model $Model Model using this behavior. - * @return bool true. - */ - protected function _setRuntimeData(Model $Model) { - $locale = $this->_getLocale($Model); - if (empty($locale)) { - return true; - } - $fields = array_merge($this->settings[$Model->alias], $this->runtime[$Model->alias]['fields']); - $tempData = array(); - - foreach ($fields as $key => $value) { - $field = (is_numeric($key)) ? $value : $key; - - if (isset($Model->data[$Model->alias][$field])) { - $tempData[$field] = $Model->data[$Model->alias][$field]; - if (is_array($Model->data[$Model->alias][$field])) { - if (is_string($locale) && !empty($Model->data[$Model->alias][$field][$locale])) { - $Model->data[$Model->alias][$field] = $Model->data[$Model->alias][$field][$locale]; - } else { - $values = array_values($Model->data[$Model->alias][$field]); - $Model->data[$Model->alias][$field] = $values[0]; - } - } - } - } - $this->runtime[$Model->alias]['beforeSave'] = $tempData; - } - -/** - * Restores model data to the original data. - * This solves issues with saveAssociated and validate = first. - * - * @param Model $Model Model using this behavior. - * @return bool true. - */ - public function afterValidate(Model $Model) { - $Model->data[$Model->alias] = array_merge( - $Model->data[$Model->alias], - $this->runtime[$Model->alias]['beforeSave'] - ); - return true; - } - -/** - * afterSave Callback - * - * @param Model $Model Model the callback is called on - * @param bool $created Whether or not the save created a record. - * @param array $options Options passed from Model::save(). - * @return bool true. - */ - public function afterSave(Model $Model, $created, $options = array()) { - if (!isset($this->runtime[$Model->alias]['beforeValidate']) && !isset($this->runtime[$Model->alias]['beforeSave'])) { - return true; - } - if (isset($this->runtime[$Model->alias]['beforeValidate'])) { - $tempData = $this->runtime[$Model->alias]['beforeValidate']; - } else { - $tempData = $this->runtime[$Model->alias]['beforeSave']; - } - - unset($this->runtime[$Model->alias]['beforeValidate'], $this->runtime[$Model->alias]['beforeSave']); - $conditions = array('model' => $Model->name, 'foreign_key' => $Model->id); - $RuntimeModel = $this->translateModel($Model); - - if ($created) { - $tempData = $this->_prepareTranslations($Model, $tempData); - } - $locale = $this->_getLocale($Model); - $atomic = array(); - if (isset($options['atomic'])) { - $atomic = array('atomic' => $options['atomic']); - } - - foreach ($tempData as $field => $value) { - unset($conditions['content']); - $conditions['field'] = $field; - if (is_array($value)) { - $conditions['locale'] = array_keys($value); - } else { - $conditions['locale'] = $locale; - if (is_array($locale)) { - $value = array($locale[0] => $value); - } else { - $value = array($locale => $value); - } - } - $translations = $RuntimeModel->find('list', array( - 'conditions' => $conditions, - 'fields' => array( - $RuntimeModel->alias . '.locale', - $RuntimeModel->alias . '.id' - ) - )); - foreach ($value as $_locale => $_value) { - $RuntimeModel->create(); - $conditions['locale'] = $_locale; - $conditions['content'] = $_value; - if (array_key_exists($_locale, $translations)) { - $RuntimeModel->save(array( - $RuntimeModel->alias => array_merge( - $conditions, array('id' => $translations[$_locale]) - ), - $atomic - )); - } else { - $RuntimeModel->save(array($RuntimeModel->alias => $conditions), $atomic); - } - } - } - } - -/** - * Prepares the data to be saved for translated records. - * Add blank fields, and populates data for multi-locale saves. - * - * @param Model $Model Model using this behavior - * @param array $data The sparse data that was provided. - * @return array The fully populated data to save. - */ - protected function _prepareTranslations(Model $Model, $data) { - $fields = array_merge($this->settings[$Model->alias], $this->runtime[$Model->alias]['fields']); - $locales = array(); - foreach ($data as $key => $value) { - if (is_array($value)) { - $locales = array_merge($locales, array_keys($value)); - } - } - $locales = array_unique($locales); - $hasLocales = count($locales) > 0; - - foreach ($fields as $key => $field) { - if (!is_numeric($key)) { - $field = $key; - } - if ($hasLocales && !isset($data[$field])) { - $data[$field] = array_fill_keys($locales, ''); - } elseif (!isset($data[$field])) { - $data[$field] = ''; - } - } - return $data; - } - -/** - * afterDelete Callback - * - * @param Model $Model Model the callback was run on. - * @return void - */ - public function afterDelete(Model $Model) { - $RuntimeModel = $this->translateModel($Model); - $conditions = array('model' => $Model->name, 'foreign_key' => $Model->id); - $RuntimeModel->deleteAll($conditions); - } - -/** - * Get selected locale for model - * - * @param Model $Model Model the locale needs to be set/get on. - * @return mixed string or false - */ - protected function _getLocale(Model $Model) { - if (!isset($Model->locale) || $Model->locale === null) { - $I18n = I18n::getInstance(); - $I18n->l10n->get(Configure::read('Config.language')); - $Model->locale = $I18n->l10n->locale; - } - - return $Model->locale; - } - -/** - * Get instance of model for translations. - * - * If the model has a translateModel property set, this will be used as the class - * name to find/use. If no translateModel property is found 'I18nModel' will be used. - * - * @param Model $Model Model to get a translatemodel for. - * @return Model - */ - public function translateModel(Model $Model) { - if (!isset($this->runtime[$Model->alias]['model'])) { - if (!isset($Model->translateModel) || empty($Model->translateModel)) { - $className = 'I18nModel'; - } else { - $className = $Model->translateModel; - } - - $this->runtime[$Model->alias]['model'] = ClassRegistry::init($className); - } - if (!empty($Model->translateTable) && $Model->translateTable !== $this->runtime[$Model->alias]['model']->useTable) { - $this->runtime[$Model->alias]['model']->setSource($Model->translateTable); - } elseif (empty($Model->translateTable) && empty($Model->translateModel)) { - $this->runtime[$Model->alias]['model']->setSource('i18n'); - } - return $this->runtime[$Model->alias]['model']; - } - -/** - * Bind translation for fields, optionally with hasMany association for - * fake field. - * - * *Note* You should avoid binding translations that overlap existing model properties. - * This can cause un-expected and un-desirable behavior. - * - * @param Model $Model using this behavior of model - * @param string|array $fields string with field or array(field1, field2=>AssocName, field3) - * @param bool $reset Leave true to have the fields only modified for the next operation. - * if false the field will be added for all future queries. - * @return bool - * @throws CakeException when attempting to bind a translating called name. This is not allowed - * as it shadows Model::$name. - */ - public function bindTranslation(Model $Model, $fields, $reset = true) { - if (is_string($fields)) { - $fields = array($fields); - } - $associations = array(); - $RuntimeModel = $this->translateModel($Model); - $default = array( - 'className' => $RuntimeModel->alias, - 'foreignKey' => 'foreign_key', - 'order' => 'id' - ); - - foreach ($fields as $key => $value) { - if (is_numeric($key)) { - $field = $value; - $association = null; - } else { - $field = $key; - $association = $value; - } - if ($association === 'name') { - throw new CakeException( - __d('cake_dev', 'You cannot bind a translation named "name".') - ); - } - $this->_removeField($Model, $field); - - if ($association === null) { - if ($reset) { - $this->runtime[$Model->alias]['fields'][] = $field; - } else { - $this->settings[$Model->alias][] = $field; - } - } else { - if ($reset) { - $this->runtime[$Model->alias]['fields'][$field] = $association; - $this->runtime[$Model->alias]['restoreFields'][] = $field; - } else { - $this->settings[$Model->alias][$field] = $association; - } - - foreach (array('hasOne', 'hasMany', 'belongsTo', 'hasAndBelongsToMany') as $type) { - if (isset($Model->{$type}[$association]) || isset($Model->__backAssociation[$type][$association])) { - trigger_error( - __d('cake_dev', 'Association %s is already bound to model %s', $association, $Model->alias), - E_USER_ERROR - ); - return false; - } - } - $associations[$association] = array_merge($default, array('conditions' => array( - 'model' => $Model->name, - $RuntimeModel->displayField => $field - ))); - } - } - - if (!empty($associations)) { - $Model->bindModel(array('hasMany' => $associations), $reset); - } - return true; - } - -/** - * Update runtime setting for a given field. - * - * @param Model $Model Model using this behavior - * @param string $field The field to update. - * @return void - */ - protected function _removeField(Model $Model, $field) { - if (array_key_exists($field, $this->settings[$Model->alias])) { - unset($this->settings[$Model->alias][$field]); - } elseif (in_array($field, $this->settings[$Model->alias])) { - $this->settings[$Model->alias] = array_merge(array_diff($this->settings[$Model->alias], array($field))); - } - - if (array_key_exists($field, $this->runtime[$Model->alias]['fields'])) { - unset($this->runtime[$Model->alias]['fields'][$field]); - } elseif (in_array($field, $this->runtime[$Model->alias]['fields'])) { - $this->runtime[$Model->alias]['fields'] = array_merge(array_diff($this->runtime[$Model->alias]['fields'], array($field))); - } - } - -/** - * Unbind translation for fields, optionally unbinds hasMany association for - * fake field - * - * @param Model $Model using this behavior of model - * @param string|array $fields string with field, or array(field1, field2=>AssocName, field3), or null for - * unbind all original translations - * @return bool - */ - public function unbindTranslation(Model $Model, $fields = null) { - if (empty($fields) && empty($this->settings[$Model->alias])) { - return false; - } - if (empty($fields)) { - return $this->unbindTranslation($Model, $this->settings[$Model->alias]); - } - - if (is_string($fields)) { - $fields = array($fields); - } - $associations = array(); - - foreach ($fields as $key => $value) { - if (is_numeric($key)) { - $field = $value; - $association = null; - } else { - $field = $key; - $association = $value; - } - - $this->_removeField($Model, $field); - - if ($association !== null && (isset($Model->hasMany[$association]) || isset($Model->__backAssociation['hasMany'][$association]))) { - $associations[] = $association; - } - } - - if (!empty($associations)) { - $Model->unbindModel(array('hasMany' => $associations), false); - } - return true; - } +class TranslateBehavior extends ModelBehavior +{ + + /** + * Used for runtime configuration of model + * + * @var array + */ + public $runtime = []; + + /** + * Stores the joinTable object for generating joins. + * + * @var object + */ + protected $_joinTable; + + /** + * Stores the runtime model for generating joins. + * + * @var Model + */ + protected $_runtimeModel; + + /** + * Callback + * + * $config for TranslateBehavior should be + * array('fields' => array('field_one', + * 'field_two' => 'FieldAssoc', 'field_three')) + * + * With above example only one permanent hasMany will be joined (for field_two + * as FieldAssoc) + * + * $config could be empty - and translations configured dynamically by + * bindTranslation() method + * + * By default INNER joins are used to fetch translations. In order to use + * other join types $config should contain 'joinType' key: + * ``` + * array( + * 'fields' => array('field_one', 'field_two' => 'FieldAssoc', 'field_three'), + * 'joinType' => 'LEFT', + * ) + * ``` + * In a model it may be configured this way: + * ``` + * public $actsAs = array( + * 'Translate' => array( + * 'content', + * 'title', + * 'joinType' => 'LEFT', + * ), + * ); + * ``` + * + * @param Model $Model Model the behavior is being attached to. + * @param array $config Array of configuration information. + * @return mixed + */ + public function setup(Model $Model, $config = []) + { + $db = ConnectionManager::getDataSource($Model->useDbConfig); + if (!$db->connected) { + trigger_error( + __d('cake_dev', 'Datasource %s for TranslateBehavior of model %s is not connected', $Model->useDbConfig, $Model->alias), + E_USER_ERROR + ); + return false; + } + + $this->settings[$Model->alias] = []; + $this->runtime[$Model->alias] = [ + 'fields' => [], + 'joinType' => 'INNER', + ]; + if (isset($config['joinType'])) { + $this->runtime[$Model->alias]['joinType'] = $config['joinType']; + unset($config['joinType']); + } + $this->translateModel($Model); + return $this->bindTranslation($Model, $config, false); + } + + /** + * Get instance of model for translations. + * + * If the model has a translateModel property set, this will be used as the class + * name to find/use. If no translateModel property is found 'I18nModel' will be used. + * + * @param Model $Model Model to get a translatemodel for. + * @return Model + */ + public function translateModel(Model $Model) + { + if (!isset($this->runtime[$Model->alias]['model'])) { + if (!isset($Model->translateModel) || empty($Model->translateModel)) { + $className = 'I18nModel'; + } else { + $className = $Model->translateModel; + } + + $this->runtime[$Model->alias]['model'] = ClassRegistry::init($className); + } + if (!empty($Model->translateTable) && $Model->translateTable !== $this->runtime[$Model->alias]['model']->useTable) { + $this->runtime[$Model->alias]['model']->setSource($Model->translateTable); + } else if (empty($Model->translateTable) && empty($Model->translateModel)) { + $this->runtime[$Model->alias]['model']->setSource('i18n'); + } + return $this->runtime[$Model->alias]['model']; + } + + /** + * Bind translation for fields, optionally with hasMany association for + * fake field. + * + * *Note* You should avoid binding translations that overlap existing model properties. + * This can cause un-expected and un-desirable behavior. + * + * @param Model $Model using this behavior of model + * @param string|array $fields string with field or array(field1, field2=>AssocName, field3) + * @param bool $reset Leave true to have the fields only modified for the next operation. + * if false the field will be added for all future queries. + * @return bool + * @throws CakeException when attempting to bind a translating called name. This is not allowed + * as it shadows Model::$name. + */ + public function bindTranslation(Model $Model, $fields, $reset = true) + { + if (is_string($fields)) { + $fields = [$fields]; + } + $associations = []; + $RuntimeModel = $this->translateModel($Model); + $default = [ + 'className' => $RuntimeModel->alias, + 'foreignKey' => 'foreign_key', + 'order' => 'id' + ]; + + foreach ($fields as $key => $value) { + if (is_numeric($key)) { + $field = $value; + $association = null; + } else { + $field = $key; + $association = $value; + } + if ($association === 'name') { + throw new CakeException( + __d('cake_dev', 'You cannot bind a translation named "name".') + ); + } + $this->_removeField($Model, $field); + + if ($association === null) { + if ($reset) { + $this->runtime[$Model->alias]['fields'][] = $field; + } else { + $this->settings[$Model->alias][] = $field; + } + } else { + if ($reset) { + $this->runtime[$Model->alias]['fields'][$field] = $association; + $this->runtime[$Model->alias]['restoreFields'][] = $field; + } else { + $this->settings[$Model->alias][$field] = $association; + } + + foreach (['hasOne', 'hasMany', 'belongsTo', 'hasAndBelongsToMany'] as $type) { + if (isset($Model->{$type}[$association]) || isset($Model->__backAssociation[$type][$association])) { + trigger_error( + __d('cake_dev', 'Association %s is already bound to model %s', $association, $Model->alias), + E_USER_ERROR + ); + return false; + } + } + $associations[$association] = array_merge($default, ['conditions' => [ + 'model' => $Model->name, + $RuntimeModel->displayField => $field + ]]); + } + } + + if (!empty($associations)) { + $Model->bindModel(['hasMany' => $associations], $reset); + } + return true; + } + + /** + * Update runtime setting for a given field. + * + * @param Model $Model Model using this behavior + * @param string $field The field to update. + * @return void + */ + protected function _removeField(Model $Model, $field) + { + if (array_key_exists($field, $this->settings[$Model->alias])) { + unset($this->settings[$Model->alias][$field]); + } else if (in_array($field, $this->settings[$Model->alias])) { + $this->settings[$Model->alias] = array_merge(array_diff($this->settings[$Model->alias], [$field])); + } + + if (array_key_exists($field, $this->runtime[$Model->alias]['fields'])) { + unset($this->runtime[$Model->alias]['fields'][$field]); + } else if (in_array($field, $this->runtime[$Model->alias]['fields'])) { + $this->runtime[$Model->alias]['fields'] = array_merge(array_diff($this->runtime[$Model->alias]['fields'], [$field])); + } + } + + /** + * Cleanup Callback unbinds bound translations and deletes setting information. + * + * @param Model $Model Model being detached. + * @return void + */ + public function cleanup(Model $Model) + { + $this->unbindTranslation($Model); + unset($this->settings[$Model->alias]); + unset($this->runtime[$Model->alias]); + } + + /** + * Unbind translation for fields, optionally unbinds hasMany association for + * fake field + * + * @param Model $Model using this behavior of model + * @param string|array $fields string with field, or array(field1, field2=>AssocName, field3), or null for + * unbind all original translations + * @return bool + */ + public function unbindTranslation(Model $Model, $fields = null) + { + if (empty($fields) && empty($this->settings[$Model->alias])) { + return false; + } + if (empty($fields)) { + return $this->unbindTranslation($Model, $this->settings[$Model->alias]); + } + + if (is_string($fields)) { + $fields = [$fields]; + } + $associations = []; + + foreach ($fields as $key => $value) { + if (is_numeric($key)) { + $field = $value; + $association = null; + } else { + $field = $key; + $association = $value; + } + + $this->_removeField($Model, $field); + + if ($association !== null && (isset($Model->hasMany[$association]) || isset($Model->__backAssociation['hasMany'][$association]))) { + $associations[] = $association; + } + } + + if (!empty($associations)) { + $Model->unbindModel(['hasMany' => $associations], false); + } + return true; + } + + /** + * beforeFind Callback + * + * @param Model $Model Model find is being run on. + * @param array $query Array of Query parameters. + * @return array Modified query + */ + public function beforeFind(Model $Model, $query) + { + $this->runtime[$Model->alias]['virtualFields'] = $Model->virtualFields; + $locale = $this->_getLocale($Model); + if (empty($locale)) { + return $query; + } + $db = $Model->getDataSource(); + $RuntimeModel = $this->translateModel($Model); + + if (!empty($RuntimeModel->tablePrefix)) { + $tablePrefix = $RuntimeModel->tablePrefix; + } else { + $tablePrefix = $db->config['prefix']; + } + $joinTable = new StdClass(); + $joinTable->tablePrefix = $tablePrefix; + $joinTable->table = $RuntimeModel->table; + $joinTable->schemaName = $RuntimeModel->getDataSource()->getSchemaName(); + + $this->_joinTable = $joinTable; + $this->_runtimeModel = $RuntimeModel; + + if (is_string($query['fields'])) { + if ($query['fields'] === "COUNT(*) AS {$db->name('count')}") { + $query['fields'] = "COUNT(DISTINCT({$db->name($Model->escapeField())})) {$db->alias}count"; + $query['joins'][] = [ + 'type' => $this->runtime[$Model->alias]['joinType'], + 'alias' => $RuntimeModel->alias, + 'table' => $joinTable, + 'conditions' => [ + $Model->escapeField() => $db->identifier($RuntimeModel->escapeField('foreign_key')), + $RuntimeModel->escapeField('model') => $Model->name, + $RuntimeModel->escapeField('locale') => $locale + ] + ]; + $conditionFields = $this->_checkConditions($Model, $query); + foreach ($conditionFields as $field) { + $query = $this->_addJoin($Model, $query, $field, $field, $locale); + } + unset($this->_joinTable, $this->_runtimeModel); + return $query; + } else { + $query['fields'] = CakeText::tokenize($query['fields']); + } + } + $addFields = $this->_getFields($Model, $query); + $this->runtime[$Model->alias]['virtualFields'] = $Model->virtualFields; + $query = $this->_addAllJoins($Model, $query, $addFields); + $this->runtime[$Model->alias]['beforeFind'] = $addFields; + unset($this->_joinTable, $this->_runtimeModel); + return $query; + } + + /** + * Get selected locale for model + * + * @param Model $Model Model the locale needs to be set/get on. + * @return mixed string or false + */ + protected function _getLocale(Model $Model) + { + if (!isset($Model->locale) || $Model->locale === null) { + $I18n = I18n::getInstance(); + $I18n->l10n->get(Configure::read('Config.language')); + $Model->locale = $I18n->l10n->locale; + } + + return $Model->locale; + } + + /** + * Check a query's conditions for translated fields. + * Return an array of translated fields found in the conditions. + * + * @param Model $Model The model being read. + * @param array $query The query array. + * @return array The list of translated fields that are in the conditions. + */ + protected function _checkConditions(Model $Model, $query) + { + if (empty($query['conditions']) || (!empty($query['conditions']) && !is_array($query['conditions']))) { + return []; + } + return $this->_getConditionFields($Model, $query['conditions']); + } + + /** + * Extracts condition field names recursively. + * + * @param Model $Model The model being read. + * @param array $conditions The conditions array. + * @return array The list of condition fields. + */ + protected function _getConditionFields(Model $Model, $conditions) + { + $conditionFields = []; + foreach ($conditions as $col => $val) { + if (is_array($val)) { + $subConditionFields = $this->_getConditionFields($Model, $val); + $conditionFields = array_merge($conditionFields, $subConditionFields); + } + foreach ($this->settings[$Model->alias] as $field => $assoc) { + if (is_numeric($field)) { + $field = $assoc; + } + if (strpos($col, $field) !== false) { + $conditionFields[] = $field; + } + } + } + return $conditionFields; + } + + /** + * Appends a join for translated fields. + * + * @param Model $Model The model being worked on. + * @param array $query The query array to append a join to. + * @param string $field The field name being joined. + * @param string $aliasField The aliased field name being joined. + * @param string|array $locale The locale(s) having joins added. + * @return array The modified query + */ + protected function _addJoin(Model $Model, $query, $field, $aliasField, $locale) + { + $db = ConnectionManager::getDataSource($Model->useDbConfig); + $RuntimeModel = $this->_runtimeModel; + $joinTable = $this->_joinTable; + $aliasVirtual = "i18n_{$field}"; + $alias = "I18n__{$field}"; + if (is_array($locale)) { + foreach ($locale as $_locale) { + $aliasVirtualLocale = "{$aliasVirtual}_{$_locale}"; + $aliasLocale = "{$alias}__{$_locale}"; + $Model->virtualFields[$aliasVirtualLocale] = "{$aliasLocale}.content"; + if (!empty($query['fields']) && is_array($query['fields'])) { + $query['fields'][] = $aliasVirtualLocale; + } + $query['joins'][] = [ + 'type' => 'LEFT', + 'alias' => $aliasLocale, + 'table' => $joinTable, + 'conditions' => [ + $Model->escapeField() => $db->identifier("{$aliasLocale}.foreign_key"), + "{$aliasLocale}.model" => $Model->name, + "{$aliasLocale}.{$RuntimeModel->displayField}" => $aliasField, + "{$aliasLocale}.locale" => $_locale + ] + ]; + } + } else { + $Model->virtualFields[$aliasVirtual] = "{$alias}.content"; + if (!empty($query['fields']) && is_array($query['fields'])) { + $query['fields'][] = $aliasVirtual; + } + $query['joins'][] = [ + 'type' => $this->runtime[$Model->alias]['joinType'], + 'alias' => $alias, + 'table' => $joinTable, + 'conditions' => [ + "{$Model->alias}.{$Model->primaryKey}" => $db->identifier("{$alias}.foreign_key"), + "{$alias}.model" => $Model->name, + "{$alias}.{$RuntimeModel->displayField}" => $aliasField, + "{$alias}.locale" => $locale + ] + ]; + } + return $query; + } + + /** + * Gets fields to be retrieved. + * + * @param Model $Model The model being worked on. + * @param array $query The query array to take fields from. + * @return array The fields. + */ + protected function _getFields(Model $Model, $query) + { + $fields = array_merge( + $this->settings[$Model->alias], + $this->runtime[$Model->alias]['fields'] + ); + $addFields = []; + if (empty($query['fields'])) { + $addFields = $fields; + } else if (is_array($query['fields'])) { + $isAllFields = ( + in_array($Model->alias . '.' . '*', $query['fields']) || + in_array($Model->escapeField('*'), $query['fields']) + ); + foreach ($fields as $key => $value) { + $field = (is_numeric($key)) ? $value : $key; + if ($isAllFields || + in_array($Model->alias . '.' . $field, $query['fields']) || + in_array($field, $query['fields']) + ) { + $addFields[] = $field; + } + } + } + return $addFields; + } + + /** + * Appends all necessary joins for translated fields. + * + * @param Model $Model The model being worked on. + * @param array $query The query array to append joins to. + * @param array $addFields The fields being joined. + * @return array The modified query + */ + protected function _addAllJoins(Model $Model, $query, $addFields) + { + $locale = $this->_getLocale($Model); + if ($addFields) { + foreach ($addFields as $_f => $field) { + $aliasField = is_numeric($_f) ? $field : $_f; + foreach ([$aliasField, $Model->alias . '.' . $aliasField] as $_field) { + $key = array_search($_field, (array)$query['fields']); + if ($key !== false) { + unset($query['fields'][$key]); + } + } + $query = $this->_addJoin($Model, $query, $field, $aliasField, $locale); + } + } + return $query; + } + + /** + * afterFind Callback + * + * @param Model $Model Model find was run on + * @param array $results Array of model results. + * @param bool $primary Did the find originate on $model. + * @return array Modified results + */ + public function afterFind(Model $Model, $results, $primary = false) + { + $Model->virtualFields = $this->runtime[$Model->alias]['virtualFields']; + + $this->runtime[$Model->alias]['virtualFields'] = []; + if (!empty($this->runtime[$Model->alias]['restoreFields'])) { + $this->runtime[$Model->alias]['fields'] = $this->runtime[$Model->alias]['restoreFields']; + unset($this->runtime[$Model->alias]['restoreFields']); + } + + $locale = $this->_getLocale($Model); + + if (empty($locale) || empty($results) || empty($this->runtime[$Model->alias]['beforeFind'])) { + return $results; + } + $beforeFind = $this->runtime[$Model->alias]['beforeFind']; + + foreach ($results as $key => &$row) { + $results[$key][$Model->alias]['locale'] = (is_array($locale)) ? current($locale) : $locale; + foreach ($beforeFind as $_f => $field) { + $aliasField = is_numeric($_f) ? $field : $_f; + $aliasVirtual = "i18n_{$field}"; + if (is_array($locale)) { + foreach ($locale as $_locale) { + $aliasVirtualLocale = "{$aliasVirtual}_{$_locale}"; + if (!isset($row[$Model->alias][$aliasField]) && !empty($row[$Model->alias][$aliasVirtualLocale])) { + $row[$Model->alias][$aliasField] = $row[$Model->alias][$aliasVirtualLocale]; + $row[$Model->alias]['locale'] = $_locale; + } + unset($row[$Model->alias][$aliasVirtualLocale]); + } + + if (!isset($row[$Model->alias][$aliasField])) { + $row[$Model->alias][$aliasField] = ''; + } + } else { + $value = ''; + if (isset($row[$Model->alias][$aliasVirtual])) { + $value = $row[$Model->alias][$aliasVirtual]; + } + $row[$Model->alias][$aliasField] = $value; + unset($row[$Model->alias][$aliasVirtual]); + } + } + } + return $results; + } + + /** + * beforeValidate Callback + * + * @param Model $Model Model invalidFields was called on. + * @param array $options Options passed from Model::save(). + * @return bool + * @see Model::save() + */ + public function beforeValidate(Model $Model, $options = []) + { + unset($this->runtime[$Model->alias]['beforeSave']); + $this->_setRuntimeData($Model); + return true; + } + + /** + * Sets the runtime data. + * + * Used from beforeValidate() and beforeSave() for compatibility issues, + * and to allow translations to be persisted even when validation + * is disabled. + * + * @param Model $Model Model using this behavior. + * @return bool true. + */ + protected function _setRuntimeData(Model $Model) + { + $locale = $this->_getLocale($Model); + if (empty($locale)) { + return true; + } + $fields = array_merge($this->settings[$Model->alias], $this->runtime[$Model->alias]['fields']); + $tempData = []; + + foreach ($fields as $key => $value) { + $field = (is_numeric($key)) ? $value : $key; + + if (isset($Model->data[$Model->alias][$field])) { + $tempData[$field] = $Model->data[$Model->alias][$field]; + if (is_array($Model->data[$Model->alias][$field])) { + if (is_string($locale) && !empty($Model->data[$Model->alias][$field][$locale])) { + $Model->data[$Model->alias][$field] = $Model->data[$Model->alias][$field][$locale]; + } else { + $values = array_values($Model->data[$Model->alias][$field]); + $Model->data[$Model->alias][$field] = $values[0]; + } + } + } + } + $this->runtime[$Model->alias]['beforeSave'] = $tempData; + } + + /** + * beforeSave callback. + * + * Copies data into the runtime property when `$options['validate']` is + * disabled. Or the runtime data hasn't been set yet. + * + * @param Model $Model Model save was called on. + * @param array $options Options passed from Model::save(). + * @return bool true. + * @see Model::save() + */ + public function beforeSave(Model $Model, $options = []) + { + if (isset($options['validate']) && !$options['validate']) { + unset($this->runtime[$Model->alias]['beforeSave']); + } + if (isset($this->runtime[$Model->alias]['beforeSave'])) { + return true; + } + $this->_setRuntimeData($Model); + return true; + } + + /** + * Restores model data to the original data. + * This solves issues with saveAssociated and validate = first. + * + * @param Model $Model Model using this behavior. + * @return bool true. + */ + public function afterValidate(Model $Model) + { + $Model->data[$Model->alias] = array_merge( + $Model->data[$Model->alias], + $this->runtime[$Model->alias]['beforeSave'] + ); + return true; + } + + /** + * afterSave Callback + * + * @param Model $Model Model the callback is called on + * @param bool $created Whether or not the save created a record. + * @param array $options Options passed from Model::save(). + * @return bool true. + */ + public function afterSave(Model $Model, $created, $options = []) + { + if (!isset($this->runtime[$Model->alias]['beforeValidate']) && !isset($this->runtime[$Model->alias]['beforeSave'])) { + return true; + } + if (isset($this->runtime[$Model->alias]['beforeValidate'])) { + $tempData = $this->runtime[$Model->alias]['beforeValidate']; + } else { + $tempData = $this->runtime[$Model->alias]['beforeSave']; + } + + unset($this->runtime[$Model->alias]['beforeValidate'], $this->runtime[$Model->alias]['beforeSave']); + $conditions = ['model' => $Model->name, 'foreign_key' => $Model->id]; + $RuntimeModel = $this->translateModel($Model); + + if ($created) { + $tempData = $this->_prepareTranslations($Model, $tempData); + } + $locale = $this->_getLocale($Model); + $atomic = []; + if (isset($options['atomic'])) { + $atomic = ['atomic' => $options['atomic']]; + } + + foreach ($tempData as $field => $value) { + unset($conditions['content']); + $conditions['field'] = $field; + if (is_array($value)) { + $conditions['locale'] = array_keys($value); + } else { + $conditions['locale'] = $locale; + if (is_array($locale)) { + $value = [$locale[0] => $value]; + } else { + $value = [$locale => $value]; + } + } + $translations = $RuntimeModel->find('list', [ + 'conditions' => $conditions, + 'fields' => [ + $RuntimeModel->alias . '.locale', + $RuntimeModel->alias . '.id' + ] + ]); + foreach ($value as $_locale => $_value) { + $RuntimeModel->create(); + $conditions['locale'] = $_locale; + $conditions['content'] = $_value; + if (array_key_exists($_locale, $translations)) { + $RuntimeModel->save([ + $RuntimeModel->alias => array_merge( + $conditions, ['id' => $translations[$_locale]] + ), + $atomic + ]); + } else { + $RuntimeModel->save([$RuntimeModel->alias => $conditions], $atomic); + } + } + } + } + + /** + * Prepares the data to be saved for translated records. + * Add blank fields, and populates data for multi-locale saves. + * + * @param Model $Model Model using this behavior + * @param array $data The sparse data that was provided. + * @return array The fully populated data to save. + */ + protected function _prepareTranslations(Model $Model, $data) + { + $fields = array_merge($this->settings[$Model->alias], $this->runtime[$Model->alias]['fields']); + $locales = []; + foreach ($data as $key => $value) { + if (is_array($value)) { + $locales = array_merge($locales, array_keys($value)); + } + } + $locales = array_unique($locales); + $hasLocales = count($locales) > 0; + + foreach ($fields as $key => $field) { + if (!is_numeric($key)) { + $field = $key; + } + if ($hasLocales && !isset($data[$field])) { + $data[$field] = array_fill_keys($locales, ''); + } else if (!isset($data[$field])) { + $data[$field] = ''; + } + } + return $data; + } + + /** + * afterDelete Callback + * + * @param Model $Model Model the callback was run on. + * @return void + */ + public function afterDelete(Model $Model) + { + $RuntimeModel = $this->translateModel($Model); + $conditions = ['model' => $Model->name, 'foreign_key' => $Model->id]; + $RuntimeModel->deleteAll($conditions); + } } diff --git a/lib/Cake/Model/Behavior/TreeBehavior.php b/lib/Cake/Model/Behavior/TreeBehavior.php index d944007b..0e59d8b2 100755 --- a/lib/Cake/Model/Behavior/TreeBehavior.php +++ b/lib/Cake/Model/Behavior/TreeBehavior.php @@ -29,1225 +29,1253 @@ * @package Cake.Model.Behavior * @link https://book.cakephp.org/2.0/en/core-libraries/behaviors/tree.html */ -class TreeBehavior extends ModelBehavior { - -/** - * Errors - * - * @var array - */ - public $errors = array(); - -/** - * Defaults - * - * @var array - */ - protected $_defaults = array( - 'parent' => 'parent_id', 'left' => 'lft', 'right' => 'rght', 'level' => null, - 'scope' => '1 = 1', 'type' => 'nested', '__parentChange' => false, 'recursive' => -1 - ); - -/** - * Used to preserve state between delete callbacks. - * - * @var array - */ - protected $_deletedRow = array(); - -/** - * Initiate Tree behavior - * - * @param Model $Model using this behavior of model - * @param array $config array of configuration settings. - * @return void - */ - public function setup(Model $Model, $config = array()) { - if (isset($config[0])) { - $config['type'] = $config[0]; - unset($config[0]); - } - $settings = $config + $this->_defaults; - - if (in_array($settings['scope'], $Model->getAssociated('belongsTo'))) { - $data = $Model->getAssociated($settings['scope']); - $Parent = $Model->{$settings['scope']}; - $settings['scope'] = $Model->escapeField($data['foreignKey']) . ' = ' . $Parent->escapeField(); - $settings['recursive'] = 0; - } - $this->settings[$Model->alias] = $settings; - } - -/** - * After save method. Called after all saves - * - * Overridden to transparently manage setting the lft and rght fields if and only if the parent field is included in the - * parameters to be saved. - * - * @param Model $Model Model using this behavior. - * @param bool $created indicates whether the node just saved was created or updated - * @param array $options Options passed from Model::save(). - * @return bool true on success, false on failure - */ - public function afterSave(Model $Model, $created, $options = array()) { - extract($this->settings[$Model->alias]); - if ($created) { - if ((isset($Model->data[$Model->alias][$parent])) && $Model->data[$Model->alias][$parent]) { - return $this->_setParent($Model, $Model->data[$Model->alias][$parent], $created); - } - } elseif ($this->settings[$Model->alias]['__parentChange']) { - $this->settings[$Model->alias]['__parentChange'] = false; - if ($level) { - $this->_setChildrenLevel($Model, $Model->id); - } - return $this->_setParent($Model, $Model->data[$Model->alias][$parent]); - } - } - -/** - * Set level for descendents. - * - * @param Model $Model Model using this behavior. - * @param int|string $id Record ID - * @return void - */ - protected function _setChildrenLevel(Model $Model, $id) { - $settings = $this->settings[$Model->alias]; - $primaryKey = $Model->primaryKey; - $depths = array($id => (int)$Model->data[$Model->alias][$settings['level']]); - - $children = $this->children( - $Model, - $id, - false, - array($primaryKey, $settings['parent'], $settings['level']), - $settings['left'], - null, - 1, - -1 - ); - - foreach ($children as $node) { - $parentIdValue = $node[$Model->alias][$settings['parent']]; - $depth = (int)$depths[$parentIdValue] + 1; - $depths[$node[$Model->alias][$primaryKey]] = $depth; - - $Model->updateAll( - array($Model->escapeField($settings['level']) => $depth), - array($Model->escapeField($primaryKey) => $node[$Model->alias][$primaryKey]) - ); - } - } - -/** - * Runs before a find() operation - * - * @param Model $Model Model using the behavior - * @param array $query Query parameters as set by cake - * @return array - */ - public function beforeFind(Model $Model, $query) { - if ($Model->findQueryType === 'threaded' && !isset($query['parent'])) { - $query['parent'] = $this->settings[$Model->alias]['parent']; - } - return $query; - } - -/** - * Stores the record about to be deleted. - * - * This is used to delete child nodes in the afterDelete. - * - * @param Model $Model Model using this behavior. - * @param bool $cascade If true records that depend on this record will also be deleted - * @return bool - */ - public function beforeDelete(Model $Model, $cascade = true) { - extract($this->settings[$Model->alias]); - $data = $Model->find('first', array( - 'conditions' => array($Model->escapeField($Model->primaryKey) => $Model->id), - 'fields' => array($Model->escapeField($left), $Model->escapeField($right)), - 'order' => false, - 'recursive' => -1)); - if ($data) { - $this->_deletedRow[$Model->alias] = current($data); - } - return true; - } - -/** - * After delete method. - * - * Will delete the current node and all children using the deleteAll method and sync the table - * - * @param Model $Model Model using this behavior - * @return bool true to continue, false to abort the delete - */ - public function afterDelete(Model $Model) { - extract($this->settings[$Model->alias]); - $data = $this->_deletedRow[$Model->alias]; - $this->_deletedRow[$Model->alias] = null; - - if (!$data[$right] || !$data[$left]) { - return true; - } - $diff = $data[$right] - $data[$left] + 1; - - if ($diff > 2) { - if (is_string($scope)) { - $scope = array($scope); - } - $scope[][$Model->escapeField($left) . " BETWEEN ? AND ?"] = array($data[$left] + 1, $data[$right] - 1); - $Model->deleteAll($scope); - } - $this->_sync($Model, $diff, '-', '> ' . $data[$right]); - return true; - } - -/** - * Before save method. Called before all saves - * - * Overridden to transparently manage setting the lft and rght fields if and only if the parent field is included in the - * parameters to be saved. For newly created nodes with NO parent the left and right field values are set directly by - * this method bypassing the setParent logic. - * - * @param Model $Model Model using this behavior - * @param array $options Options passed from Model::save(). - * @return bool true to continue, false to abort the save - * @see Model::save() - */ - public function beforeSave(Model $Model, $options = array()) { - extract($this->settings[$Model->alias]); - - $this->_addToWhitelist($Model, array($left, $right)); - if ($level) { - $this->_addToWhitelist($Model, $level); - } - $parentIsSet = array_key_exists($parent, $Model->data[$Model->alias]); - - if (!$Model->id || !$Model->exists($Model->getID())) { - if ($parentIsSet && $Model->data[$Model->alias][$parent]) { - $parentNode = $this->_getNode($Model, $Model->data[$Model->alias][$parent]); - if (!$parentNode) { - return false; - } - - $Model->data[$Model->alias][$left] = 0; - $Model->data[$Model->alias][$right] = 0; - if ($level) { - $Model->data[$Model->alias][$level] = (int)$parentNode[$Model->alias][$level] + 1; - } - return true; - } - - $edge = $this->_getMax($Model, $scope, $right, $recursive); - $Model->data[$Model->alias][$left] = $edge + 1; - $Model->data[$Model->alias][$right] = $edge + 2; - if ($level) { - $Model->data[$Model->alias][$level] = 0; - } - return true; - } - - if ($parentIsSet) { - if ($Model->data[$Model->alias][$parent] != $Model->field($parent)) { - $this->settings[$Model->alias]['__parentChange'] = true; - } - if (!$Model->data[$Model->alias][$parent]) { - $Model->data[$Model->alias][$parent] = null; - $this->_addToWhitelist($Model, $parent); - if ($level) { - $Model->data[$Model->alias][$level] = 0; - } - return true; - } - - $values = $this->_getNode($Model, $Model->id); - if (empty($values)) { - return false; - } - list($node) = array_values($values); - - $parentNode = $this->_getNode($Model, $Model->data[$Model->alias][$parent]); - if (!$parentNode) { - return false; - } - list($parentNode) = array_values($parentNode); - - if (($node[$left] < $parentNode[$left]) && ($parentNode[$right] < $node[$right])) { - return false; - } - if ($node[$Model->primaryKey] === $parentNode[$Model->primaryKey]) { - return false; - } - if ($level) { - $Model->data[$Model->alias][$level] = (int)$parentNode[$level] + 1; - } - } - - return true; - } - -/** - * Returns a single node from the tree from its primary key - * - * @param Model $Model Model using this behavior - * @param int|string $id The ID of the record to read - * @return array|bool The record read or false - */ - protected function _getNode(Model $Model, $id) { - $settings = $this->settings[$Model->alias]; - $fields = array($Model->primaryKey, $settings['parent'], $settings['left'], $settings['right']); - if ($settings['level']) { - $fields[] = $settings['level']; - } - - return $Model->find('first', array( - 'conditions' => array($Model->escapeField() => $id), - 'fields' => $fields, - 'recursive' => $settings['recursive'], - 'order' => false, - )); - } - -/** - * Get the number of child nodes - * - * If the direct parameter is set to true, only the direct children are counted (based upon the parent_id field) - * If false is passed for the id parameter, all top level nodes are counted, or all nodes are counted. - * - * @param Model $Model Model using this behavior - * @param int|string|bool $id The ID of the record to read or false to read all top level nodes - * @param bool $direct whether to count direct, or all, children - * @return int number of child nodes - * @link https://book.cakephp.org/2.0/en/core-libraries/behaviors/tree.html#TreeBehavior::childCount - */ - public function childCount(Model $Model, $id = null, $direct = false) { - if (is_array($id)) { - extract(array_merge(array('id' => null), $id)); - } - if ($id === null && $Model->id) { - $id = $Model->id; - } elseif (!$id) { - $id = null; - } - extract($this->settings[$Model->alias]); - - if ($direct) { - return $Model->find('count', array('conditions' => array($scope, $Model->escapeField($parent) => $id))); - } - - if ($id === null) { - return $Model->find('count', array('conditions' => $scope)); - } elseif ($Model->id === $id && isset($Model->data[$Model->alias][$left]) && isset($Model->data[$Model->alias][$right])) { - $data = $Model->data[$Model->alias]; - } else { - $data = $this->_getNode($Model, $id); - if (!$data) { - return 0; - } - $data = $data[$Model->alias]; - } - return ($data[$right] - $data[$left] - 1) / 2; - } - -/** - * Get the child nodes of the current model - * - * If the direct parameter is set to true, only the direct children are returned (based upon the parent_id field) - * If false is passed for the id parameter, top level, or all (depending on direct parameter appropriate) are counted. - * - * @param Model $Model Model using this behavior - * @param int|string $id The ID of the record to read - * @param bool $direct whether to return only the direct, or all, children - * @param string|array $fields Either a single string of a field name, or an array of field names - * @param string $order SQL ORDER BY conditions (e.g. "price DESC" or "name ASC") defaults to the tree order - * @param int $limit SQL LIMIT clause, for calculating items per page. - * @param int $page Page number, for accessing paged data - * @param int $recursive The number of levels deep to fetch associated records - * @return array Array of child nodes - * @link https://book.cakephp.org/2.0/en/core-libraries/behaviors/tree.html#TreeBehavior::children - */ - public function children(Model $Model, $id = null, $direct = false, $fields = null, $order = null, $limit = null, $page = 1, $recursive = null) { - $options = array(); - if (is_array($id)) { - $options = $this->_getOptions($id); - extract(array_merge(array('id' => null), $id)); - } - $overrideRecursive = $recursive; - - if ($id === null && $Model->id) { - $id = $Model->id; - } elseif (!$id) { - $id = null; - } - - extract($this->settings[$Model->alias]); - - if ($overrideRecursive !== null) { - $recursive = $overrideRecursive; - } - if (!$order) { - $order = $Model->escapeField($left) . " asc"; - } - if ($direct) { - $conditions = array($scope, $Model->escapeField($parent) => $id); - return $Model->find('all', compact('conditions', 'fields', 'order', 'limit', 'page', 'recursive')); - } - - if (!$id) { - $conditions = $scope; - } else { - $result = array_values((array)$Model->find('first', array( - 'conditions' => array($scope, $Model->escapeField() => $id), - 'fields' => array($left, $right), - 'recursive' => $recursive, - 'order' => false, - ))); - - if (empty($result) || !isset($result[0])) { - return array(); - } - $conditions = array($scope, - $Model->escapeField($right) . ' <' => $result[0][$right], - $Model->escapeField($left) . ' >' => $result[0][$left] - ); - } - $options = array_merge(compact( - 'conditions', 'fields', 'order', 'limit', 'page', 'recursive' - ), $options); - return $Model->find('all', $options); - } - -/** - * A convenience method for returning a hierarchical array used for HTML select boxes - * - * @param Model $Model Model using this behavior - * @param string|array $conditions SQL conditions as a string or as an array('field' =>'value',...) - * @param string $keyPath A string path to the key, i.e. "{n}.Post.id" - * @param string $valuePath A string path to the value, i.e. "{n}.Post.title" - * @param string $spacer The character or characters which will be repeated - * @param int $recursive The number of levels deep to fetch associated records - * @return array An associative array of records, where the id is the key, and the display field is the value - * @link https://book.cakephp.org/2.0/en/core-libraries/behaviors/tree.html#TreeBehavior::generateTreeList - */ - public function generateTreeList(Model $Model, $conditions = null, $keyPath = null, $valuePath = null, $spacer = '_', $recursive = null) { - $overrideRecursive = $recursive; - extract($this->settings[$Model->alias]); - if ($overrideRecursive !== null) { - $recursive = $overrideRecursive; - } - - $fields = null; - if (!$keyPath && !$valuePath && $Model->hasField($Model->displayField)) { - $fields = array($Model->primaryKey, $Model->displayField, $left, $right); - } - - $conditions = (array)$conditions; - if ($scope) { - $conditions[] = $scope; - } - - $order = $Model->escapeField($left) . ' asc'; - $results = $Model->find('all', compact('conditions', 'fields', 'order', 'recursive')); - - return $this->formatTreeList($Model, $results, compact('keyPath', 'valuePath', 'spacer')); - } - -/** - * Formats result of a find() call to a hierarchical array used for HTML select boxes. - * - * Note that when using your own find() call this expects the order to be "left" field asc in order - * to generate the same result as using generateTreeList() directly. - * - * Options: - * - * - 'keyPath': A string path to the key, i.e. "{n}.Post.id" - * - 'valuePath': A string path to the value, i.e. "{n}.Post.title" - * - 'spacer': The character or characters which will be repeated - * - * @param Model $Model Model using this behavior - * @param array $results Result array of a find() call - * @param array $options Options - * @return array An associative array of records, where the id is the key, and the display field is the value - */ - public function formatTreeList(Model $Model, array $results, array $options = array()) { - if (empty($results)) { - return array(); - } - $defaults = array( - 'keyPath' => null, - 'valuePath' => null, - 'spacer' => '_' - ); - $options += $defaults; - - extract($this->settings[$Model->alias]); - - if (!$options['keyPath']) { - $options['keyPath'] = '{n}.' . $Model->alias . '.' . $Model->primaryKey; - } - - if (!$options['valuePath']) { - $options['valuePath'] = array('%s%s', '{n}.tree_prefix', '{n}.' . $Model->alias . '.' . $Model->displayField); - - } elseif (is_string($options['valuePath'])) { - $options['valuePath'] = array('%s%s', '{n}.tree_prefix', $options['valuePath']); - - } else { - array_unshift($options['valuePath'], '%s' . $options['valuePath'][0], '{n}.tree_prefix'); - } - - $stack = array(); - - foreach ($results as $i => $result) { - $count = count($stack); - while ($stack && ($stack[$count - 1] < $result[$Model->alias][$right])) { - array_pop($stack); - $count--; - } - $results[$i]['tree_prefix'] = str_repeat($options['spacer'], $count); - $stack[] = $result[$Model->alias][$right]; - } - - return Hash::combine($results, $options['keyPath'], $options['valuePath']); - } - -/** - * Get the parent node - * - * reads the parent id and returns this node - * - * @param Model $Model Model using this behavior - * @param int|string $id The ID of the record to read - * @param string|array $fields Fields to get - * @param int $recursive The number of levels deep to fetch associated records - * @return array|bool Array of data for the parent node - * @link https://book.cakephp.org/2.0/en/core-libraries/behaviors/tree.html#TreeBehavior::getParentNode - */ - public function getParentNode(Model $Model, $id = null, $fields = null, $recursive = null) { - $options = array(); - if (is_array($id)) { - $options = $this->_getOptions($id); - extract(array_merge(array('id' => null), $id)); - } - $overrideRecursive = $recursive; - if (empty($id)) { - $id = $Model->id; - } - extract($this->settings[$Model->alias]); - if ($overrideRecursive !== null) { - $recursive = $overrideRecursive; - } - $parentId = $Model->find('first', array( - 'conditions' => array($Model->primaryKey => $id), - 'fields' => array($parent), - 'order' => false, - 'recursive' => -1 - )); - - if ($parentId) { - $parentId = $parentId[$Model->alias][$parent]; - $options = array_merge(array( - 'conditions' => array($Model->escapeField() => $parentId), - 'fields' => $fields, - 'order' => false, - 'recursive' => $recursive - ), $options); - $parent = $Model->find('first', $options); - - return $parent; - } - return false; - } - -/** - * Convenience method to create default find() options from $arg when it is an - * associative array. - * - * @param array $arg Array - * @return array Options array - */ - protected function _getOptions($arg) { - return count(array_filter(array_keys($arg), 'is_string')) > 0 ? - $arg : - array(); - } - -/** - * Get the path to the given node - * - * @param Model $Model Model using this behavior - * @param int|string|null $id The ID of the record to read - * @param string|array|null $fields Either a single string of a field name, or an array of field names - * @param int|null $recursive The number of levels deep to fetch associated records - * @return array Array of nodes from top most parent to current node - * @link https://book.cakephp.org/2.0/en/core-libraries/behaviors/tree.html#TreeBehavior::getPath - */ - public function getPath(Model $Model, $id = null, $fields = null, $recursive = null) { - $options = array(); - if (is_array($id)) { - $options = $this->_getOptions($id); - extract(array_merge(array('id' => null), $id)); - } - - if (!empty($options)) { - $fields = null; - if (!empty($options['fields'])) { - $fields = $options['fields']; - } - if (!empty($options['recursive'])) { - $recursive = $options['recursive']; - } - } - $overrideRecursive = $recursive; - if (empty($id)) { - $id = $Model->id; - } - extract($this->settings[$Model->alias]); - if ($overrideRecursive !== null) { - $recursive = $overrideRecursive; - } - $result = $Model->find('first', array( - 'conditions' => array($Model->escapeField() => $id), - 'fields' => array($left, $right), - 'order' => false, - 'recursive' => $recursive - )); - if ($result) { - $result = array_values($result); - } else { - return array(); - } - $item = $result[0]; - $options = array_merge(array( - 'conditions' => array( - $scope, - $Model->escapeField($left) . ' <=' => $item[$left], - $Model->escapeField($right) . ' >=' => $item[$right], - ), - 'fields' => $fields, - 'order' => array($Model->escapeField($left) => 'asc'), - 'recursive' => $recursive - ), $options); - $results = $Model->find('all', $options); - return $results; - } - -/** - * Reorder the node without changing the parent. - * - * If the node is the last child, or is a top level node with no subsequent node this method will return false - * - * @param Model $Model Model using this behavior - * @param int|string|null $id The ID of the record to move - * @param int|bool $number how many places to move the node or true to move to last position - * @return bool true on success, false on failure - * @link https://book.cakephp.org/2.0/en/core-libraries/behaviors/tree.html#TreeBehavior::moveDown - */ - public function moveDown(Model $Model, $id = null, $number = 1) { - if (is_array($id)) { - extract(array_merge(array('id' => null), $id)); - } - if (!$number) { - return false; - } - if (empty($id)) { - $id = $Model->id; - } - extract($this->settings[$Model->alias]); - list($node) = array_values($this->_getNode($Model, $id)); - if ($node[$parent]) { - list($parentNode) = array_values($this->_getNode($Model, $node[$parent])); - if (($node[$right] + 1) == $parentNode[$right]) { - return false; - } - } - $nextNode = $Model->find('first', array( - 'conditions' => array($scope, $Model->escapeField($left) => ($node[$right] + 1)), - 'fields' => array($Model->primaryKey, $left, $right), - 'order' => false, - 'recursive' => $recursive) - ); - if ($nextNode) { - list($nextNode) = array_values($nextNode); - } else { - return false; - } - $edge = $this->_getMax($Model, $scope, $right, $recursive); - $this->_sync($Model, $edge - $node[$left] + 1, '+', 'BETWEEN ' . $node[$left] . ' AND ' . $node[$right]); - $this->_sync($Model, $nextNode[$left] - $node[$left], '-', 'BETWEEN ' . $nextNode[$left] . ' AND ' . $nextNode[$right]); - $this->_sync($Model, $edge - $node[$left] - ($nextNode[$right] - $nextNode[$left]), '-', '> ' . $edge); - - if (is_int($number)) { - $number--; - } - if ($number) { - $this->moveDown($Model, $id, $number); - } - return true; - } - -/** - * Reorder the node without changing the parent. - * - * If the node is the first child, or is a top level node with no previous node this method will return false - * - * @param Model $Model Model using this behavior - * @param int|string|null $id The ID of the record to move - * @param int|bool $number how many places to move the node, or true to move to first position - * @return bool true on success, false on failure - * @link https://book.cakephp.org/2.0/en/core-libraries/behaviors/tree.html#TreeBehavior::moveUp - */ - public function moveUp(Model $Model, $id = null, $number = 1) { - if (is_array($id)) { - extract(array_merge(array('id' => null), $id)); - } - if (!$number) { - return false; - } - if (empty($id)) { - $id = $Model->id; - } - extract($this->settings[$Model->alias]); - list($node) = array_values($this->_getNode($Model, $id)); - if ($node[$parent]) { - list($parentNode) = array_values($this->_getNode($Model, $node[$parent])); - if (($node[$left] - 1) == $parentNode[$left]) { - return false; - } - } - $previousNode = $Model->find('first', array( - 'conditions' => array($scope, $Model->escapeField($right) => ($node[$left] - 1)), - 'fields' => array($Model->primaryKey, $left, $right), - 'order' => false, - 'recursive' => $recursive - )); - - if ($previousNode) { - list($previousNode) = array_values($previousNode); - } else { - return false; - } - $edge = $this->_getMax($Model, $scope, $right, $recursive); - $this->_sync($Model, $edge - $previousNode[$left] + 1, '+', 'BETWEEN ' . $previousNode[$left] . ' AND ' . $previousNode[$right]); - $this->_sync($Model, $node[$left] - $previousNode[$left], '-', 'BETWEEN ' . $node[$left] . ' AND ' . $node[$right]); - $this->_sync($Model, $edge - $previousNode[$left] - ($node[$right] - $node[$left]), '-', '> ' . $edge); - if (is_int($number)) { - $number--; - } - if ($number) { - $this->moveUp($Model, $id, $number); - } - return true; - } - -/** - * Recover a corrupted tree - * - * The mode parameter is used to specify the source of info that is valid/correct. The opposite source of data - * will be populated based upon that source of info. E.g. if the MPTT fields are corrupt or empty, with the $mode - * 'parent' the values of the parent_id field will be used to populate the left and right fields. The missingParentAction - * parameter only applies to "parent" mode and determines what to do if the parent field contains an id that is not present. - * - * @param Model $Model Model using this behavior - * @param string $mode parent or tree - * @param string|int|null $missingParentAction 'return' to do nothing and return, 'delete' to - * delete, or the id of the parent to set as the parent_id - * @return bool true on success, false on failure - * @link https://book.cakephp.org/2.0/en/core-libraries/behaviors/tree.html#TreeBehavior::recover - */ - public function recover(Model $Model, $mode = 'parent', $missingParentAction = null) { - if (is_array($mode)) { - extract(array_merge(array('mode' => 'parent'), $mode)); - } - extract($this->settings[$Model->alias]); - $Model->recursive = $recursive; - if ($mode === 'parent') { - $Model->bindModel(array('belongsTo' => array('VerifyParent' => array( - 'className' => $Model->name, - 'foreignKey' => $parent, - 'fields' => array($Model->primaryKey, $left, $right, $parent), - )))); - $missingParents = $Model->find('list', array( - 'recursive' => 0, - 'conditions' => array($scope, array( - 'NOT' => array($Model->escapeField($parent) => null), $Model->VerifyParent->escapeField() => null - )), - 'order' => false, - )); - $Model->unbindModel(array('belongsTo' => array('VerifyParent'))); - if ($missingParents) { - if ($missingParentAction === 'return') { - foreach ($missingParents as $id => $display) { - $this->errors[] = 'cannot find the parent for ' . $Model->alias . ' with id ' . $id . '(' . $display . ')'; - } - return false; - } elseif ($missingParentAction === 'delete') { - $Model->deleteAll(array($Model->escapeField($Model->primaryKey) => array_flip($missingParents)), false); - } else { - $Model->updateAll(array($Model->escapeField($parent) => $missingParentAction), array($Model->escapeField($Model->primaryKey) => array_flip($missingParents))); - } - } - - $this->_recoverByParentId($Model); - } else { - $db = ConnectionManager::getDataSource($Model->useDbConfig); - foreach ($Model->find('all', array('conditions' => $scope, 'fields' => array($Model->primaryKey, $parent), 'order' => $left)) as $array) { - $path = $this->getPath($Model, $array[$Model->alias][$Model->primaryKey]); - $parentId = null; - if (count($path) > 1) { - $parentId = $path[count($path) - 2][$Model->alias][$Model->primaryKey]; - } - $Model->updateAll(array($parent => $db->value($parentId, $parent)), array($Model->escapeField() => $array[$Model->alias][$Model->primaryKey])); - } - } - return true; - } - -/** - * _recoverByParentId - * - * Recursive helper function used by recover - * - * @param Model $Model Model instance. - * @param int $counter Counter - * @param int|string|null $parentId Parent record Id - * @return int counter - */ - protected function _recoverByParentId(Model $Model, $counter = 1, $parentId = null) { - $params = array( - 'conditions' => array( - $this->settings[$Model->alias]['parent'] => $parentId - ), - 'fields' => array($Model->primaryKey), - 'page' => 1, - 'limit' => 100, - 'order' => array($Model->primaryKey) - ); - - $scope = $this->settings[$Model->alias]['scope']; - if ($scope && ($scope !== '1 = 1' && $scope !== true)) { - $params['conditions'][] = $scope; - } - - $children = $Model->find('all', $params); - $hasChildren = (bool)$children; - - if ($parentId !== null) { - if ($hasChildren) { - $Model->updateAll( - array($this->settings[$Model->alias]['left'] => $counter), - array($Model->escapeField() => $parentId) - ); - $counter++; - } else { - $Model->updateAll( - array( - $this->settings[$Model->alias]['left'] => $counter, - $this->settings[$Model->alias]['right'] => $counter + 1 - ), - array($Model->escapeField() => $parentId) - ); - $counter += 2; - } - } - - while ($children) { - foreach ($children as $row) { - $counter = $this->_recoverByParentId($Model, $counter, $row[$Model->alias][$Model->primaryKey]); - } - - if (count($children) !== $params['limit']) { - break; - } - $params['page']++; - $children = $Model->find('all', $params); - } - - if ($parentId !== null && $hasChildren) { - $Model->updateAll( - array($this->settings[$Model->alias]['right'] => $counter), - array($Model->escapeField() => $parentId) - ); - $counter++; - } - - return $counter; - } - -/** - * Reorder method. - * - * Reorders the nodes (and child nodes) of the tree according to the field and direction specified in the parameters. - * This method does not change the parent of any node. - * - * Requires a valid tree, by default it verifies the tree before beginning. - * - * Options: - * - * - 'id' id of record to use as top node for reordering - * - 'field' Which field to use in reordering defaults to displayField - * - 'order' Direction to order either DESC or ASC (defaults to ASC) - * - 'verify' Whether or not to verify the tree before reorder. defaults to true. - * - * @param Model $Model Model using this behavior - * @param array $options array of options to use in reordering. - * @return bool true on success, false on failure - * @link https://book.cakephp.org/2.0/en/core-libraries/behaviors/tree.html#TreeBehavior::reorder - */ - public function reorder(Model $Model, $options = array()) { - $options += array('id' => null, 'field' => $Model->displayField, 'order' => 'ASC', 'verify' => true); - extract($options); - if ($verify && !$this->verify($Model)) { - return false; - } - $verify = false; - extract($this->settings[$Model->alias]); - $fields = array($Model->primaryKey, $field, $left, $right); - $sort = $field . ' ' . $order; - $nodes = $this->children($Model, $id, true, $fields, $sort, null, null, $recursive); - - $cacheQueries = $Model->cacheQueries; - $Model->cacheQueries = false; - if ($nodes) { - foreach ($nodes as $node) { - $id = $node[$Model->alias][$Model->primaryKey]; - $this->moveDown($Model, $id, true); - if ($node[$Model->alias][$left] != $node[$Model->alias][$right] - 1) { - $this->reorder($Model, compact('id', 'field', 'order', 'verify')); - } - } - } - $Model->cacheQueries = $cacheQueries; - return true; - } - -/** - * Remove the current node from the tree, and reparent all children up one level. - * - * If the parameter delete is false, the node will become a new top level node. Otherwise the node will be deleted - * after the children are reparented. - * - * @param Model $Model Model using this behavior - * @param int|string|null $id The ID of the record to remove - * @param bool $delete whether to delete the node after reparenting children (if any) - * @return bool true on success, false on failure - * @link https://book.cakephp.org/2.0/en/core-libraries/behaviors/tree.html#TreeBehavior::removeFromTree - */ - public function removeFromTree(Model $Model, $id = null, $delete = false) { - if (is_array($id)) { - extract(array_merge(array('id' => null), $id)); - } - extract($this->settings[$Model->alias]); - - list($node) = array_values($this->_getNode($Model, $id)); - - if ($node[$right] == $node[$left] + 1) { - if ($delete) { - return $Model->delete($id); - } - $Model->id = $id; - return $Model->saveField($parent, null); - } elseif ($node[$parent]) { - list($parentNode) = array_values($this->_getNode($Model, $node[$parent])); - } else { - $parentNode[$right] = $node[$right] + 1; - } - - $db = ConnectionManager::getDataSource($Model->useDbConfig); - $Model->updateAll( - array($parent => $db->value($node[$parent], $parent)), - array($Model->escapeField($parent) => $node[$Model->primaryKey]) - ); - $this->_sync($Model, 1, '-', 'BETWEEN ' . ($node[$left] + 1) . ' AND ' . ($node[$right] - 1)); - $this->_sync($Model, 2, '-', '> ' . ($node[$right])); - $Model->id = $id; - - if ($delete) { - $Model->updateAll( - array( - $Model->escapeField($left) => 0, - $Model->escapeField($right) => 0, - $Model->escapeField($parent) => null - ), - array($Model->escapeField() => $id) - ); - return $Model->delete($id); - } - $edge = $this->_getMax($Model, $scope, $right, $recursive); - if ($node[$right] == $edge) { - $edge = $edge - 2; - } - $Model->id = $id; - return $Model->save( - array($left => $edge + 1, $right => $edge + 2, $parent => null), - array('callbacks' => false, 'validate' => false) - ); - } - -/** - * Check if the current tree is valid. - * - * Returns true if the tree is valid otherwise an array of (type, incorrect left/right index, message) - * - * @param Model $Model Model using this behavior - * @return mixed true if the tree is valid or empty, otherwise an array of (error type [index, node], - * [incorrect left/right index,node id], message) - * @link https://book.cakephp.org/2.0/en/core-libraries/behaviors/tree.html#TreeBehavior::verify - */ - public function verify(Model $Model) { - extract($this->settings[$Model->alias]); - if (!$Model->find('count', array('conditions' => $scope))) { - return true; - } - $min = $this->_getMin($Model, $scope, $left, $recursive); - $edge = $this->_getMax($Model, $scope, $right, $recursive); - $errors = array(); - - for ($i = $min; $i <= $edge; $i++) { - $count = $Model->find('count', array('conditions' => array( - $scope, 'OR' => array($Model->escapeField($left) => $i, $Model->escapeField($right) => $i) - ))); - if ($count != 1) { - if (!$count) { - $errors[] = array('index', $i, 'missing'); - } else { - $errors[] = array('index', $i, 'duplicate'); - } - } - } - $node = $Model->find('first', array( - 'conditions' => array($scope, $Model->escapeField($right) . '< ' . $Model->escapeField($left)), - 'order' => false, - 'recursive' => 0 - )); - if ($node) { - $errors[] = array('node', $node[$Model->alias][$Model->primaryKey], 'left greater than right.'); - } - - $Model->bindModel(array('belongsTo' => array('VerifyParent' => array( - 'className' => $Model->name, - 'foreignKey' => $parent, - 'fields' => array($Model->primaryKey, $left, $right, $parent) - )))); - - $rows = $Model->find('all', array('conditions' => $scope, 'recursive' => 0)); - foreach ($rows as $instance) { - if ($instance[$Model->alias][$left] === null || $instance[$Model->alias][$right] === null) { - $errors[] = array('node', $instance[$Model->alias][$Model->primaryKey], - 'has invalid left or right values'); - } elseif ($instance[$Model->alias][$left] == $instance[$Model->alias][$right]) { - $errors[] = array('node', $instance[$Model->alias][$Model->primaryKey], - 'left and right values identical'); - } elseif ($instance[$Model->alias][$parent]) { - if (!$instance['VerifyParent'][$Model->primaryKey]) { - $errors[] = array('node', $instance[$Model->alias][$Model->primaryKey], - 'The parent node ' . $instance[$Model->alias][$parent] . ' doesn\'t exist'); - } elseif ($instance[$Model->alias][$left] < $instance['VerifyParent'][$left]) { - $errors[] = array('node', $instance[$Model->alias][$Model->primaryKey], - 'left less than parent (node ' . $instance['VerifyParent'][$Model->primaryKey] . ').'); - } elseif ($instance[$Model->alias][$right] > $instance['VerifyParent'][$right]) { - $errors[] = array('node', $instance[$Model->alias][$Model->primaryKey], - 'right greater than parent (node ' . $instance['VerifyParent'][$Model->primaryKey] . ').'); - } - } elseif ($Model->find('count', array('conditions' => array($scope, $Model->escapeField($left) . ' <' => $instance[$Model->alias][$left], $Model->escapeField($right) . ' >' => $instance[$Model->alias][$right]), 'recursive' => 0))) { - $errors[] = array('node', $instance[$Model->alias][$Model->primaryKey], 'The parent field is blank, but has a parent'); - } - } - if ($errors) { - return $errors; - } - return true; - } - -/** - * Returns the depth level of a node in the tree. - * - * @param Model $Model Model using this behavior - * @param int|string|null $id The primary key for record to get the level of. - * @return int|bool Integer of the level or false if the node does not exist. - */ - public function getLevel(Model $Model, $id = null) { - if ($id === null) { - $id = $Model->id; - } - - $node = $Model->find('first', array( - 'conditions' => array($Model->escapeField() => $id), - 'order' => false, - 'recursive' => -1 - )); - - if (empty($node)) { - return false; - } - - extract($this->settings[$Model->alias]); - - return $Model->find('count', array( - 'conditions' => array( - $scope, - $left . ' <' => $node[$Model->alias][$left], - $right . ' >' => $node[$Model->alias][$right] - ), - 'order' => false, - 'recursive' => -1 - )); - } - -/** - * Sets the parent of the given node - * - * The force parameter is used to override the "don't change the parent to the current parent" logic in the event - * of recovering a corrupted table, or creating new nodes. Otherwise it should always be false. In reality this - * method could be private, since calling save with parent_id set also calls setParent - * - * @param Model $Model Model using this behavior - * @param int|string|null $parentId Parent record Id - * @param bool $created True if newly created record else false. - * @return bool true on success, false on failure - */ - protected function _setParent(Model $Model, $parentId = null, $created = false) { - extract($this->settings[$Model->alias]); - list($node) = array_values($this->_getNode($Model, $Model->id)); - $edge = $this->_getMax($Model, $scope, $right, $recursive, $created); - - if (empty($parentId)) { - $this->_sync($Model, $edge - $node[$left] + 1, '+', 'BETWEEN ' . $node[$left] . ' AND ' . $node[$right], $created); - $this->_sync($Model, $node[$right] - $node[$left] + 1, '-', '> ' . $node[$left], $created); - } else { - $values = $this->_getNode($Model, $parentId); - - if ($values === false) { - return false; - } - $parentNode = array_values($values); - - if (empty($parentNode) || empty($parentNode[0])) { - return false; - } - $parentNode = $parentNode[0]; - - if (($Model->id === $parentId)) { - return false; - } elseif (($node[$left] < $parentNode[$left]) && ($parentNode[$right] < $node[$right])) { - return false; - } - if (empty($node[$left]) && empty($node[$right])) { - $this->_sync($Model, 2, '+', '>= ' . $parentNode[$right], $created); - $result = $Model->save( - array($left => $parentNode[$right], $right => $parentNode[$right] + 1, $parent => $parentId), - array('validate' => false, 'callbacks' => false) - ); - $Model->data = $result; - } else { - $this->_sync($Model, $edge - $node[$left] + 1, '+', 'BETWEEN ' . $node[$left] . ' AND ' . $node[$right], $created); - $diff = $node[$right] - $node[$left] + 1; - - if ($node[$left] > $parentNode[$left]) { - if ($node[$right] < $parentNode[$right]) { - $this->_sync($Model, $diff, '-', 'BETWEEN ' . $node[$right] . ' AND ' . ($parentNode[$right] - 1), $created); - $this->_sync($Model, $edge - $parentNode[$right] + $diff + 1, '-', '> ' . $edge, $created); - } else { - $this->_sync($Model, $diff, '+', 'BETWEEN ' . $parentNode[$right] . ' AND ' . $node[$right], $created); - $this->_sync($Model, $edge - $parentNode[$right] + 1, '-', '> ' . $edge, $created); - } - } else { - $this->_sync($Model, $diff, '-', 'BETWEEN ' . $node[$right] . ' AND ' . ($parentNode[$right] - 1), $created); - $this->_sync($Model, $edge - $parentNode[$right] + $diff + 1, '-', '> ' . $edge, $created); - } - } - } - return true; - } - -/** - * get the maximum index value in the table. - * - * @param Model $Model Model Instance. - * @param string $scope Scoping conditions. - * @param string $right Right value - * @param int $recursive Recursive find value. - * @param bool $created Whether it's a new record. - * @return int - */ - protected function _getMax(Model $Model, $scope, $right, $recursive = -1, $created = false) { - $db = ConnectionManager::getDataSource($Model->useDbConfig); - if ($created) { - if (is_string($scope)) { - $scope .= " AND " . $Model->escapeField() . " <> "; - $scope .= $db->value($Model->id, $Model->getColumnType($Model->primaryKey)); - } else { - $scope['NOT'][$Model->alias . '.' . $Model->primaryKey] = $Model->id; - } - } - $name = $Model->escapeField($right); - list($edge) = array_values($Model->find('first', array( - 'conditions' => $scope, - 'fields' => $db->calculate($Model, 'max', array($name, $right)), - 'recursive' => $recursive, - 'order' => false, - 'callbacks' => false - ))); - return (empty($edge[$right])) ? 0 : $edge[$right]; - } - -/** - * get the minimum index value in the table. - * - * @param Model $Model Model instance. - * @param string $scope Scoping conditions. - * @param string $left Left value. - * @param int $recursive Recurursive find value. - * @return int - */ - protected function _getMin(Model $Model, $scope, $left, $recursive = -1) { - $db = ConnectionManager::getDataSource($Model->useDbConfig); - $name = $Model->escapeField($left); - list($edge) = array_values($Model->find('first', array( - 'conditions' => $scope, - 'fields' => $db->calculate($Model, 'min', array($name, $left)), - 'recursive' => $recursive, - 'order' => false, - 'callbacks' => false - ))); - return (empty($edge[$left])) ? 0 : $edge[$left]; - } - -/** - * Table sync method. - * - * Handles table sync operations, Taking account of the behavior scope. - * - * @param Model $Model Model instance. - * @param int $shift Shift by. - * @param string $dir Direction. - * @param array $conditions Conditions. - * @param bool $created Whether it's a new record. - * @param string $field Field type. - * @return void - */ - protected function _sync(Model $Model, $shift, $dir = '+', $conditions = array(), $created = false, $field = 'both') { - $ModelRecursive = $Model->recursive; - extract($this->settings[$Model->alias]); - $Model->recursive = $recursive; - - if ($field === 'both') { - $this->_sync($Model, $shift, $dir, $conditions, $created, $left); - $field = $right; - } - if (is_string($conditions)) { - $conditions = array($Model->escapeField($field) . " {$conditions}"); - } - if (($scope !== '1 = 1' && $scope !== true) && $scope) { - $conditions[] = $scope; - } - if ($created) { - $conditions['NOT'][$Model->escapeField()] = $Model->id; - } - $Model->updateAll(array($Model->escapeField($field) => $Model->escapeField($field) . ' ' . $dir . ' ' . $shift), $conditions); - $Model->recursive = $ModelRecursive; - } +class TreeBehavior extends ModelBehavior +{ + + /** + * Errors + * + * @var array + */ + public $errors = []; + + /** + * Defaults + * + * @var array + */ + protected $_defaults = [ + 'parent' => 'parent_id', 'left' => 'lft', 'right' => 'rght', 'level' => null, + 'scope' => '1 = 1', 'type' => 'nested', '__parentChange' => false, 'recursive' => -1 + ]; + + /** + * Used to preserve state between delete callbacks. + * + * @var array + */ + protected $_deletedRow = []; + + /** + * Initiate Tree behavior + * + * @param Model $Model using this behavior of model + * @param array $config array of configuration settings. + * @return void + */ + public function setup(Model $Model, $config = []) + { + if (isset($config[0])) { + $config['type'] = $config[0]; + unset($config[0]); + } + $settings = $config + $this->_defaults; + + if (in_array($settings['scope'], $Model->getAssociated('belongsTo'))) { + $data = $Model->getAssociated($settings['scope']); + $Parent = $Model->{$settings['scope']}; + $settings['scope'] = $Model->escapeField($data['foreignKey']) . ' = ' . $Parent->escapeField(); + $settings['recursive'] = 0; + } + $this->settings[$Model->alias] = $settings; + } + + /** + * After save method. Called after all saves + * + * Overridden to transparently manage setting the lft and rght fields if and only if the parent field is included in the + * parameters to be saved. + * + * @param Model $Model Model using this behavior. + * @param bool $created indicates whether the node just saved was created or updated + * @param array $options Options passed from Model::save(). + * @return bool true on success, false on failure + */ + public function afterSave(Model $Model, $created, $options = []) + { + extract($this->settings[$Model->alias]); + if ($created) { + if ((isset($Model->data[$Model->alias][$parent])) && $Model->data[$Model->alias][$parent]) { + return $this->_setParent($Model, $Model->data[$Model->alias][$parent], $created); + } + } else if ($this->settings[$Model->alias]['__parentChange']) { + $this->settings[$Model->alias]['__parentChange'] = false; + if ($level) { + $this->_setChildrenLevel($Model, $Model->id); + } + return $this->_setParent($Model, $Model->data[$Model->alias][$parent]); + } + } + + /** + * Sets the parent of the given node + * + * The force parameter is used to override the "don't change the parent to the current parent" logic in the event + * of recovering a corrupted table, or creating new nodes. Otherwise it should always be false. In reality this + * method could be private, since calling save with parent_id set also calls setParent + * + * @param Model $Model Model using this behavior + * @param int|string|null $parentId Parent record Id + * @param bool $created True if newly created record else false. + * @return bool true on success, false on failure + */ + protected function _setParent(Model $Model, $parentId = null, $created = false) + { + extract($this->settings[$Model->alias]); + list($node) = array_values($this->_getNode($Model, $Model->id)); + $edge = $this->_getMax($Model, $scope, $right, $recursive, $created); + + if (empty($parentId)) { + $this->_sync($Model, $edge - $node[$left] + 1, '+', 'BETWEEN ' . $node[$left] . ' AND ' . $node[$right], $created); + $this->_sync($Model, $node[$right] - $node[$left] + 1, '-', '> ' . $node[$left], $created); + } else { + $values = $this->_getNode($Model, $parentId); + + if ($values === false) { + return false; + } + $parentNode = array_values($values); + + if (empty($parentNode) || empty($parentNode[0])) { + return false; + } + $parentNode = $parentNode[0]; + + if (($Model->id === $parentId)) { + return false; + } else if (($node[$left] < $parentNode[$left]) && ($parentNode[$right] < $node[$right])) { + return false; + } + if (empty($node[$left]) && empty($node[$right])) { + $this->_sync($Model, 2, '+', '>= ' . $parentNode[$right], $created); + $result = $Model->save( + [$left => $parentNode[$right], $right => $parentNode[$right] + 1, $parent => $parentId], + ['validate' => false, 'callbacks' => false] + ); + $Model->data = $result; + } else { + $this->_sync($Model, $edge - $node[$left] + 1, '+', 'BETWEEN ' . $node[$left] . ' AND ' . $node[$right], $created); + $diff = $node[$right] - $node[$left] + 1; + + if ($node[$left] > $parentNode[$left]) { + if ($node[$right] < $parentNode[$right]) { + $this->_sync($Model, $diff, '-', 'BETWEEN ' . $node[$right] . ' AND ' . ($parentNode[$right] - 1), $created); + $this->_sync($Model, $edge - $parentNode[$right] + $diff + 1, '-', '> ' . $edge, $created); + } else { + $this->_sync($Model, $diff, '+', 'BETWEEN ' . $parentNode[$right] . ' AND ' . $node[$right], $created); + $this->_sync($Model, $edge - $parentNode[$right] + 1, '-', '> ' . $edge, $created); + } + } else { + $this->_sync($Model, $diff, '-', 'BETWEEN ' . $node[$right] . ' AND ' . ($parentNode[$right] - 1), $created); + $this->_sync($Model, $edge - $parentNode[$right] + $diff + 1, '-', '> ' . $edge, $created); + } + } + } + return true; + } + + /** + * Returns a single node from the tree from its primary key + * + * @param Model $Model Model using this behavior + * @param int|string $id The ID of the record to read + * @return array|bool The record read or false + */ + protected function _getNode(Model $Model, $id) + { + $settings = $this->settings[$Model->alias]; + $fields = [$Model->primaryKey, $settings['parent'], $settings['left'], $settings['right']]; + if ($settings['level']) { + $fields[] = $settings['level']; + } + + return $Model->find('first', [ + 'conditions' => [$Model->escapeField() => $id], + 'fields' => $fields, + 'recursive' => $settings['recursive'], + 'order' => false, + ]); + } + + /** + * get the maximum index value in the table. + * + * @param Model $Model Model Instance. + * @param string $scope Scoping conditions. + * @param string $right Right value + * @param int $recursive Recursive find value. + * @param bool $created Whether it's a new record. + * @return int + */ + protected function _getMax(Model $Model, $scope, $right, $recursive = -1, $created = false) + { + $db = ConnectionManager::getDataSource($Model->useDbConfig); + if ($created) { + if (is_string($scope)) { + $scope .= " AND " . $Model->escapeField() . " <> "; + $scope .= $db->value($Model->id, $Model->getColumnType($Model->primaryKey)); + } else { + $scope['NOT'][$Model->alias . '.' . $Model->primaryKey] = $Model->id; + } + } + $name = $Model->escapeField($right); + list($edge) = array_values($Model->find('first', [ + 'conditions' => $scope, + 'fields' => $db->calculate($Model, 'max', [$name, $right]), + 'recursive' => $recursive, + 'order' => false, + 'callbacks' => false + ])); + return (empty($edge[$right])) ? 0 : $edge[$right]; + } + + /** + * Table sync method. + * + * Handles table sync operations, Taking account of the behavior scope. + * + * @param Model $Model Model instance. + * @param int $shift Shift by. + * @param string $dir Direction. + * @param array $conditions Conditions. + * @param bool $created Whether it's a new record. + * @param string $field Field type. + * @return void + */ + protected function _sync(Model $Model, $shift, $dir = '+', $conditions = [], $created = false, $field = 'both') + { + $ModelRecursive = $Model->recursive; + extract($this->settings[$Model->alias]); + $Model->recursive = $recursive; + + if ($field === 'both') { + $this->_sync($Model, $shift, $dir, $conditions, $created, $left); + $field = $right; + } + if (is_string($conditions)) { + $conditions = [$Model->escapeField($field) . " {$conditions}"]; + } + if (($scope !== '1 = 1' && $scope !== true) && $scope) { + $conditions[] = $scope; + } + if ($created) { + $conditions['NOT'][$Model->escapeField()] = $Model->id; + } + $Model->updateAll([$Model->escapeField($field) => $Model->escapeField($field) . ' ' . $dir . ' ' . $shift], $conditions); + $Model->recursive = $ModelRecursive; + } + + /** + * Set level for descendents. + * + * @param Model $Model Model using this behavior. + * @param int|string $id Record ID + * @return void + */ + protected function _setChildrenLevel(Model $Model, $id) + { + $settings = $this->settings[$Model->alias]; + $primaryKey = $Model->primaryKey; + $depths = [$id => (int)$Model->data[$Model->alias][$settings['level']]]; + + $children = $this->children( + $Model, + $id, + false, + [$primaryKey, $settings['parent'], $settings['level']], + $settings['left'], + null, + 1, + -1 + ); + + foreach ($children as $node) { + $parentIdValue = $node[$Model->alias][$settings['parent']]; + $depth = (int)$depths[$parentIdValue] + 1; + $depths[$node[$Model->alias][$primaryKey]] = $depth; + + $Model->updateAll( + [$Model->escapeField($settings['level']) => $depth], + [$Model->escapeField($primaryKey) => $node[$Model->alias][$primaryKey]] + ); + } + } + + /** + * Get the child nodes of the current model + * + * If the direct parameter is set to true, only the direct children are returned (based upon the parent_id field) + * If false is passed for the id parameter, top level, or all (depending on direct parameter appropriate) are counted. + * + * @param Model $Model Model using this behavior + * @param int|string $id The ID of the record to read + * @param bool $direct whether to return only the direct, or all, children + * @param string|array $fields Either a single string of a field name, or an array of field names + * @param string $order SQL ORDER BY conditions (e.g. "price DESC" or "name ASC") defaults to the tree order + * @param int $limit SQL LIMIT clause, for calculating items per page. + * @param int $page Page number, for accessing paged data + * @param int $recursive The number of levels deep to fetch associated records + * @return array Array of child nodes + * @link https://book.cakephp.org/2.0/en/core-libraries/behaviors/tree.html#TreeBehavior::children + */ + public function children(Model $Model, $id = null, $direct = false, $fields = null, $order = null, $limit = null, $page = 1, $recursive = null) + { + $options = []; + if (is_array($id)) { + $options = $this->_getOptions($id); + extract(array_merge(['id' => null], $id)); + } + $overrideRecursive = $recursive; + + if ($id === null && $Model->id) { + $id = $Model->id; + } else if (!$id) { + $id = null; + } + + extract($this->settings[$Model->alias]); + + if ($overrideRecursive !== null) { + $recursive = $overrideRecursive; + } + if (!$order) { + $order = $Model->escapeField($left) . " asc"; + } + if ($direct) { + $conditions = [$scope, $Model->escapeField($parent) => $id]; + return $Model->find('all', compact('conditions', 'fields', 'order', 'limit', 'page', 'recursive')); + } + + if (!$id) { + $conditions = $scope; + } else { + $result = array_values((array)$Model->find('first', [ + 'conditions' => [$scope, $Model->escapeField() => $id], + 'fields' => [$left, $right], + 'recursive' => $recursive, + 'order' => false, + ])); + + if (empty($result) || !isset($result[0])) { + return []; + } + $conditions = [$scope, + $Model->escapeField($right) . ' <' => $result[0][$right], + $Model->escapeField($left) . ' >' => $result[0][$left] + ]; + } + $options = array_merge(compact( + 'conditions', 'fields', 'order', 'limit', 'page', 'recursive' + ), $options); + return $Model->find('all', $options); + } + + /** + * Convenience method to create default find() options from $arg when it is an + * associative array. + * + * @param array $arg Array + * @return array Options array + */ + protected function _getOptions($arg) + { + return count(array_filter(array_keys($arg), 'is_string')) > 0 ? + $arg : + []; + } + + /** + * Runs before a find() operation + * + * @param Model $Model Model using the behavior + * @param array $query Query parameters as set by cake + * @return array + */ + public function beforeFind(Model $Model, $query) + { + if ($Model->findQueryType === 'threaded' && !isset($query['parent'])) { + $query['parent'] = $this->settings[$Model->alias]['parent']; + } + return $query; + } + + /** + * Stores the record about to be deleted. + * + * This is used to delete child nodes in the afterDelete. + * + * @param Model $Model Model using this behavior. + * @param bool $cascade If true records that depend on this record will also be deleted + * @return bool + */ + public function beforeDelete(Model $Model, $cascade = true) + { + extract($this->settings[$Model->alias]); + $data = $Model->find('first', [ + 'conditions' => [$Model->escapeField($Model->primaryKey) => $Model->id], + 'fields' => [$Model->escapeField($left), $Model->escapeField($right)], + 'order' => false, + 'recursive' => -1]); + if ($data) { + $this->_deletedRow[$Model->alias] = current($data); + } + return true; + } + + /** + * After delete method. + * + * Will delete the current node and all children using the deleteAll method and sync the table + * + * @param Model $Model Model using this behavior + * @return bool true to continue, false to abort the delete + */ + public function afterDelete(Model $Model) + { + extract($this->settings[$Model->alias]); + $data = $this->_deletedRow[$Model->alias]; + $this->_deletedRow[$Model->alias] = null; + + if (!$data[$right] || !$data[$left]) { + return true; + } + $diff = $data[$right] - $data[$left] + 1; + + if ($diff > 2) { + if (is_string($scope)) { + $scope = [$scope]; + } + $scope[][$Model->escapeField($left) . " BETWEEN ? AND ?"] = [$data[$left] + 1, $data[$right] - 1]; + $Model->deleteAll($scope); + } + $this->_sync($Model, $diff, '-', '> ' . $data[$right]); + return true; + } + + /** + * Before save method. Called before all saves + * + * Overridden to transparently manage setting the lft and rght fields if and only if the parent field is included in the + * parameters to be saved. For newly created nodes with NO parent the left and right field values are set directly by + * this method bypassing the setParent logic. + * + * @param Model $Model Model using this behavior + * @param array $options Options passed from Model::save(). + * @return bool true to continue, false to abort the save + * @see Model::save() + */ + public function beforeSave(Model $Model, $options = []) + { + extract($this->settings[$Model->alias]); + + $this->_addToWhitelist($Model, [$left, $right]); + if ($level) { + $this->_addToWhitelist($Model, $level); + } + $parentIsSet = array_key_exists($parent, $Model->data[$Model->alias]); + + if (!$Model->id || !$Model->exists($Model->getID())) { + if ($parentIsSet && $Model->data[$Model->alias][$parent]) { + $parentNode = $this->_getNode($Model, $Model->data[$Model->alias][$parent]); + if (!$parentNode) { + return false; + } + + $Model->data[$Model->alias][$left] = 0; + $Model->data[$Model->alias][$right] = 0; + if ($level) { + $Model->data[$Model->alias][$level] = (int)$parentNode[$Model->alias][$level] + 1; + } + return true; + } + + $edge = $this->_getMax($Model, $scope, $right, $recursive); + $Model->data[$Model->alias][$left] = $edge + 1; + $Model->data[$Model->alias][$right] = $edge + 2; + if ($level) { + $Model->data[$Model->alias][$level] = 0; + } + return true; + } + + if ($parentIsSet) { + if ($Model->data[$Model->alias][$parent] != $Model->field($parent)) { + $this->settings[$Model->alias]['__parentChange'] = true; + } + if (!$Model->data[$Model->alias][$parent]) { + $Model->data[$Model->alias][$parent] = null; + $this->_addToWhitelist($Model, $parent); + if ($level) { + $Model->data[$Model->alias][$level] = 0; + } + return true; + } + + $values = $this->_getNode($Model, $Model->id); + if (empty($values)) { + return false; + } + list($node) = array_values($values); + + $parentNode = $this->_getNode($Model, $Model->data[$Model->alias][$parent]); + if (!$parentNode) { + return false; + } + list($parentNode) = array_values($parentNode); + + if (($node[$left] < $parentNode[$left]) && ($parentNode[$right] < $node[$right])) { + return false; + } + if ($node[$Model->primaryKey] === $parentNode[$Model->primaryKey]) { + return false; + } + if ($level) { + $Model->data[$Model->alias][$level] = (int)$parentNode[$level] + 1; + } + } + + return true; + } + + /** + * Get the number of child nodes + * + * If the direct parameter is set to true, only the direct children are counted (based upon the parent_id field) + * If false is passed for the id parameter, all top level nodes are counted, or all nodes are counted. + * + * @param Model $Model Model using this behavior + * @param int|string|bool $id The ID of the record to read or false to read all top level nodes + * @param bool $direct whether to count direct, or all, children + * @return int number of child nodes + * @link https://book.cakephp.org/2.0/en/core-libraries/behaviors/tree.html#TreeBehavior::childCount + */ + public function childCount(Model $Model, $id = null, $direct = false) + { + if (is_array($id)) { + extract(array_merge(['id' => null], $id)); + } + if ($id === null && $Model->id) { + $id = $Model->id; + } else if (!$id) { + $id = null; + } + extract($this->settings[$Model->alias]); + + if ($direct) { + return $Model->find('count', ['conditions' => [$scope, $Model->escapeField($parent) => $id]]); + } + + if ($id === null) { + return $Model->find('count', ['conditions' => $scope]); + } else if ($Model->id === $id && isset($Model->data[$Model->alias][$left]) && isset($Model->data[$Model->alias][$right])) { + $data = $Model->data[$Model->alias]; + } else { + $data = $this->_getNode($Model, $id); + if (!$data) { + return 0; + } + $data = $data[$Model->alias]; + } + return ($data[$right] - $data[$left] - 1) / 2; + } + + /** + * A convenience method for returning a hierarchical array used for HTML select boxes + * + * @param Model $Model Model using this behavior + * @param string|array $conditions SQL conditions as a string or as an array('field' =>'value',...) + * @param string $keyPath A string path to the key, i.e. "{n}.Post.id" + * @param string $valuePath A string path to the value, i.e. "{n}.Post.title" + * @param string $spacer The character or characters which will be repeated + * @param int $recursive The number of levels deep to fetch associated records + * @return array An associative array of records, where the id is the key, and the display field is the value + * @link https://book.cakephp.org/2.0/en/core-libraries/behaviors/tree.html#TreeBehavior::generateTreeList + */ + public function generateTreeList(Model $Model, $conditions = null, $keyPath = null, $valuePath = null, $spacer = '_', $recursive = null) + { + $overrideRecursive = $recursive; + extract($this->settings[$Model->alias]); + if ($overrideRecursive !== null) { + $recursive = $overrideRecursive; + } + + $fields = null; + if (!$keyPath && !$valuePath && $Model->hasField($Model->displayField)) { + $fields = [$Model->primaryKey, $Model->displayField, $left, $right]; + } + + $conditions = (array)$conditions; + if ($scope) { + $conditions[] = $scope; + } + + $order = $Model->escapeField($left) . ' asc'; + $results = $Model->find('all', compact('conditions', 'fields', 'order', 'recursive')); + + return $this->formatTreeList($Model, $results, compact('keyPath', 'valuePath', 'spacer')); + } + + /** + * Formats result of a find() call to a hierarchical array used for HTML select boxes. + * + * Note that when using your own find() call this expects the order to be "left" field asc in order + * to generate the same result as using generateTreeList() directly. + * + * Options: + * + * - 'keyPath': A string path to the key, i.e. "{n}.Post.id" + * - 'valuePath': A string path to the value, i.e. "{n}.Post.title" + * - 'spacer': The character or characters which will be repeated + * + * @param Model $Model Model using this behavior + * @param array $results Result array of a find() call + * @param array $options Options + * @return array An associative array of records, where the id is the key, and the display field is the value + */ + public function formatTreeList(Model $Model, array $results, array $options = []) + { + if (empty($results)) { + return []; + } + $defaults = [ + 'keyPath' => null, + 'valuePath' => null, + 'spacer' => '_' + ]; + $options += $defaults; + + extract($this->settings[$Model->alias]); + + if (!$options['keyPath']) { + $options['keyPath'] = '{n}.' . $Model->alias . '.' . $Model->primaryKey; + } + + if (!$options['valuePath']) { + $options['valuePath'] = ['%s%s', '{n}.tree_prefix', '{n}.' . $Model->alias . '.' . $Model->displayField]; + + } else if (is_string($options['valuePath'])) { + $options['valuePath'] = ['%s%s', '{n}.tree_prefix', $options['valuePath']]; + + } else { + array_unshift($options['valuePath'], '%s' . $options['valuePath'][0], '{n}.tree_prefix'); + } + + $stack = []; + + foreach ($results as $i => $result) { + $count = count($stack); + while ($stack && ($stack[$count - 1] < $result[$Model->alias][$right])) { + array_pop($stack); + $count--; + } + $results[$i]['tree_prefix'] = str_repeat($options['spacer'], $count); + $stack[] = $result[$Model->alias][$right]; + } + + return Hash::combine($results, $options['keyPath'], $options['valuePath']); + } + + /** + * Get the parent node + * + * reads the parent id and returns this node + * + * @param Model $Model Model using this behavior + * @param int|string $id The ID of the record to read + * @param string|array $fields Fields to get + * @param int $recursive The number of levels deep to fetch associated records + * @return array|bool Array of data for the parent node + * @link https://book.cakephp.org/2.0/en/core-libraries/behaviors/tree.html#TreeBehavior::getParentNode + */ + public function getParentNode(Model $Model, $id = null, $fields = null, $recursive = null) + { + $options = []; + if (is_array($id)) { + $options = $this->_getOptions($id); + extract(array_merge(['id' => null], $id)); + } + $overrideRecursive = $recursive; + if (empty($id)) { + $id = $Model->id; + } + extract($this->settings[$Model->alias]); + if ($overrideRecursive !== null) { + $recursive = $overrideRecursive; + } + $parentId = $Model->find('first', [ + 'conditions' => [$Model->primaryKey => $id], + 'fields' => [$parent], + 'order' => false, + 'recursive' => -1 + ]); + + if ($parentId) { + $parentId = $parentId[$Model->alias][$parent]; + $options = array_merge([ + 'conditions' => [$Model->escapeField() => $parentId], + 'fields' => $fields, + 'order' => false, + 'recursive' => $recursive + ], $options); + $parent = $Model->find('first', $options); + + return $parent; + } + return false; + } + + /** + * Reorder the node without changing the parent. + * + * If the node is the first child, or is a top level node with no previous node this method will return false + * + * @param Model $Model Model using this behavior + * @param int|string|null $id The ID of the record to move + * @param int|bool $number how many places to move the node, or true to move to first position + * @return bool true on success, false on failure + * @link https://book.cakephp.org/2.0/en/core-libraries/behaviors/tree.html#TreeBehavior::moveUp + */ + public function moveUp(Model $Model, $id = null, $number = 1) + { + if (is_array($id)) { + extract(array_merge(['id' => null], $id)); + } + if (!$number) { + return false; + } + if (empty($id)) { + $id = $Model->id; + } + extract($this->settings[$Model->alias]); + list($node) = array_values($this->_getNode($Model, $id)); + if ($node[$parent]) { + list($parentNode) = array_values($this->_getNode($Model, $node[$parent])); + if (($node[$left] - 1) == $parentNode[$left]) { + return false; + } + } + $previousNode = $Model->find('first', [ + 'conditions' => [$scope, $Model->escapeField($right) => ($node[$left] - 1)], + 'fields' => [$Model->primaryKey, $left, $right], + 'order' => false, + 'recursive' => $recursive + ]); + + if ($previousNode) { + list($previousNode) = array_values($previousNode); + } else { + return false; + } + $edge = $this->_getMax($Model, $scope, $right, $recursive); + $this->_sync($Model, $edge - $previousNode[$left] + 1, '+', 'BETWEEN ' . $previousNode[$left] . ' AND ' . $previousNode[$right]); + $this->_sync($Model, $node[$left] - $previousNode[$left], '-', 'BETWEEN ' . $node[$left] . ' AND ' . $node[$right]); + $this->_sync($Model, $edge - $previousNode[$left] - ($node[$right] - $node[$left]), '-', '> ' . $edge); + if (is_int($number)) { + $number--; + } + if ($number) { + $this->moveUp($Model, $id, $number); + } + return true; + } + + /** + * Recover a corrupted tree + * + * The mode parameter is used to specify the source of info that is valid/correct. The opposite source of data + * will be populated based upon that source of info. E.g. if the MPTT fields are corrupt or empty, with the $mode + * 'parent' the values of the parent_id field will be used to populate the left and right fields. The missingParentAction + * parameter only applies to "parent" mode and determines what to do if the parent field contains an id that is not present. + * + * @param Model $Model Model using this behavior + * @param string $mode parent or tree + * @param string|int|null $missingParentAction 'return' to do nothing and return, 'delete' to + * delete, or the id of the parent to set as the parent_id + * @return bool true on success, false on failure + * @link https://book.cakephp.org/2.0/en/core-libraries/behaviors/tree.html#TreeBehavior::recover + */ + public function recover(Model $Model, $mode = 'parent', $missingParentAction = null) + { + if (is_array($mode)) { + extract(array_merge(['mode' => 'parent'], $mode)); + } + extract($this->settings[$Model->alias]); + $Model->recursive = $recursive; + if ($mode === 'parent') { + $Model->bindModel(['belongsTo' => ['VerifyParent' => [ + 'className' => $Model->name, + 'foreignKey' => $parent, + 'fields' => [$Model->primaryKey, $left, $right, $parent], + ]]]); + $missingParents = $Model->find('list', [ + 'recursive' => 0, + 'conditions' => [$scope, [ + 'NOT' => [$Model->escapeField($parent) => null], $Model->VerifyParent->escapeField() => null + ]], + 'order' => false, + ]); + $Model->unbindModel(['belongsTo' => ['VerifyParent']]); + if ($missingParents) { + if ($missingParentAction === 'return') { + foreach ($missingParents as $id => $display) { + $this->errors[] = 'cannot find the parent for ' . $Model->alias . ' with id ' . $id . '(' . $display . ')'; + } + return false; + } else if ($missingParentAction === 'delete') { + $Model->deleteAll([$Model->escapeField($Model->primaryKey) => array_flip($missingParents)], false); + } else { + $Model->updateAll([$Model->escapeField($parent) => $missingParentAction], [$Model->escapeField($Model->primaryKey) => array_flip($missingParents)]); + } + } + + $this->_recoverByParentId($Model); + } else { + $db = ConnectionManager::getDataSource($Model->useDbConfig); + foreach ($Model->find('all', ['conditions' => $scope, 'fields' => [$Model->primaryKey, $parent], 'order' => $left]) as $array) { + $path = $this->getPath($Model, $array[$Model->alias][$Model->primaryKey]); + $parentId = null; + if (count($path) > 1) { + $parentId = $path[count($path) - 2][$Model->alias][$Model->primaryKey]; + } + $Model->updateAll([$parent => $db->value($parentId, $parent)], [$Model->escapeField() => $array[$Model->alias][$Model->primaryKey]]); + } + } + return true; + } + + /** + * _recoverByParentId + * + * Recursive helper function used by recover + * + * @param Model $Model Model instance. + * @param int $counter Counter + * @param int|string|null $parentId Parent record Id + * @return int counter + */ + protected function _recoverByParentId(Model $Model, $counter = 1, $parentId = null) + { + $params = [ + 'conditions' => [ + $this->settings[$Model->alias]['parent'] => $parentId + ], + 'fields' => [$Model->primaryKey], + 'page' => 1, + 'limit' => 100, + 'order' => [$Model->primaryKey] + ]; + + $scope = $this->settings[$Model->alias]['scope']; + if ($scope && ($scope !== '1 = 1' && $scope !== true)) { + $params['conditions'][] = $scope; + } + + $children = $Model->find('all', $params); + $hasChildren = (bool)$children; + + if ($parentId !== null) { + if ($hasChildren) { + $Model->updateAll( + [$this->settings[$Model->alias]['left'] => $counter], + [$Model->escapeField() => $parentId] + ); + $counter++; + } else { + $Model->updateAll( + [ + $this->settings[$Model->alias]['left'] => $counter, + $this->settings[$Model->alias]['right'] => $counter + 1 + ], + [$Model->escapeField() => $parentId] + ); + $counter += 2; + } + } + + while ($children) { + foreach ($children as $row) { + $counter = $this->_recoverByParentId($Model, $counter, $row[$Model->alias][$Model->primaryKey]); + } + + if (count($children) !== $params['limit']) { + break; + } + $params['page']++; + $children = $Model->find('all', $params); + } + + if ($parentId !== null && $hasChildren) { + $Model->updateAll( + [$this->settings[$Model->alias]['right'] => $counter], + [$Model->escapeField() => $parentId] + ); + $counter++; + } + + return $counter; + } + + /** + * Get the path to the given node + * + * @param Model $Model Model using this behavior + * @param int|string|null $id The ID of the record to read + * @param string|array|null $fields Either a single string of a field name, or an array of field names + * @param int|null $recursive The number of levels deep to fetch associated records + * @return array Array of nodes from top most parent to current node + * @link https://book.cakephp.org/2.0/en/core-libraries/behaviors/tree.html#TreeBehavior::getPath + */ + public function getPath(Model $Model, $id = null, $fields = null, $recursive = null) + { + $options = []; + if (is_array($id)) { + $options = $this->_getOptions($id); + extract(array_merge(['id' => null], $id)); + } + + if (!empty($options)) { + $fields = null; + if (!empty($options['fields'])) { + $fields = $options['fields']; + } + if (!empty($options['recursive'])) { + $recursive = $options['recursive']; + } + } + $overrideRecursive = $recursive; + if (empty($id)) { + $id = $Model->id; + } + extract($this->settings[$Model->alias]); + if ($overrideRecursive !== null) { + $recursive = $overrideRecursive; + } + $result = $Model->find('first', [ + 'conditions' => [$Model->escapeField() => $id], + 'fields' => [$left, $right], + 'order' => false, + 'recursive' => $recursive + ]); + if ($result) { + $result = array_values($result); + } else { + return []; + } + $item = $result[0]; + $options = array_merge([ + 'conditions' => [ + $scope, + $Model->escapeField($left) . ' <=' => $item[$left], + $Model->escapeField($right) . ' >=' => $item[$right], + ], + 'fields' => $fields, + 'order' => [$Model->escapeField($left) => 'asc'], + 'recursive' => $recursive + ], $options); + $results = $Model->find('all', $options); + return $results; + } + + /** + * Reorder method. + * + * Reorders the nodes (and child nodes) of the tree according to the field and direction specified in the parameters. + * This method does not change the parent of any node. + * + * Requires a valid tree, by default it verifies the tree before beginning. + * + * Options: + * + * - 'id' id of record to use as top node for reordering + * - 'field' Which field to use in reordering defaults to displayField + * - 'order' Direction to order either DESC or ASC (defaults to ASC) + * - 'verify' Whether or not to verify the tree before reorder. defaults to true. + * + * @param Model $Model Model using this behavior + * @param array $options array of options to use in reordering. + * @return bool true on success, false on failure + * @link https://book.cakephp.org/2.0/en/core-libraries/behaviors/tree.html#TreeBehavior::reorder + */ + public function reorder(Model $Model, $options = []) + { + $options += ['id' => null, 'field' => $Model->displayField, 'order' => 'ASC', 'verify' => true]; + extract($options); + if ($verify && !$this->verify($Model)) { + return false; + } + $verify = false; + extract($this->settings[$Model->alias]); + $fields = [$Model->primaryKey, $field, $left, $right]; + $sort = $field . ' ' . $order; + $nodes = $this->children($Model, $id, true, $fields, $sort, null, null, $recursive); + + $cacheQueries = $Model->cacheQueries; + $Model->cacheQueries = false; + if ($nodes) { + foreach ($nodes as $node) { + $id = $node[$Model->alias][$Model->primaryKey]; + $this->moveDown($Model, $id, true); + if ($node[$Model->alias][$left] != $node[$Model->alias][$right] - 1) { + $this->reorder($Model, compact('id', 'field', 'order', 'verify')); + } + } + } + $Model->cacheQueries = $cacheQueries; + return true; + } + + /** + * Check if the current tree is valid. + * + * Returns true if the tree is valid otherwise an array of (type, incorrect left/right index, message) + * + * @param Model $Model Model using this behavior + * @return mixed true if the tree is valid or empty, otherwise an array of (error type [index, node], + * [incorrect left/right index,node id], message) + * @link https://book.cakephp.org/2.0/en/core-libraries/behaviors/tree.html#TreeBehavior::verify + */ + public function verify(Model $Model) + { + extract($this->settings[$Model->alias]); + if (!$Model->find('count', ['conditions' => $scope])) { + return true; + } + $min = $this->_getMin($Model, $scope, $left, $recursive); + $edge = $this->_getMax($Model, $scope, $right, $recursive); + $errors = []; + + for ($i = $min; $i <= $edge; $i++) { + $count = $Model->find('count', ['conditions' => [ + $scope, 'OR' => [$Model->escapeField($left) => $i, $Model->escapeField($right) => $i] + ]]); + if ($count != 1) { + if (!$count) { + $errors[] = ['index', $i, 'missing']; + } else { + $errors[] = ['index', $i, 'duplicate']; + } + } + } + $node = $Model->find('first', [ + 'conditions' => [$scope, $Model->escapeField($right) . '< ' . $Model->escapeField($left)], + 'order' => false, + 'recursive' => 0 + ]); + if ($node) { + $errors[] = ['node', $node[$Model->alias][$Model->primaryKey], 'left greater than right.']; + } + + $Model->bindModel(['belongsTo' => ['VerifyParent' => [ + 'className' => $Model->name, + 'foreignKey' => $parent, + 'fields' => [$Model->primaryKey, $left, $right, $parent] + ]]]); + + $rows = $Model->find('all', ['conditions' => $scope, 'recursive' => 0]); + foreach ($rows as $instance) { + if ($instance[$Model->alias][$left] === null || $instance[$Model->alias][$right] === null) { + $errors[] = ['node', $instance[$Model->alias][$Model->primaryKey], + 'has invalid left or right values']; + } else if ($instance[$Model->alias][$left] == $instance[$Model->alias][$right]) { + $errors[] = ['node', $instance[$Model->alias][$Model->primaryKey], + 'left and right values identical']; + } else if ($instance[$Model->alias][$parent]) { + if (!$instance['VerifyParent'][$Model->primaryKey]) { + $errors[] = ['node', $instance[$Model->alias][$Model->primaryKey], + 'The parent node ' . $instance[$Model->alias][$parent] . ' doesn\'t exist']; + } else if ($instance[$Model->alias][$left] < $instance['VerifyParent'][$left]) { + $errors[] = ['node', $instance[$Model->alias][$Model->primaryKey], + 'left less than parent (node ' . $instance['VerifyParent'][$Model->primaryKey] . ').']; + } else if ($instance[$Model->alias][$right] > $instance['VerifyParent'][$right]) { + $errors[] = ['node', $instance[$Model->alias][$Model->primaryKey], + 'right greater than parent (node ' . $instance['VerifyParent'][$Model->primaryKey] . ').']; + } + } else if ($Model->find('count', ['conditions' => [$scope, $Model->escapeField($left) . ' <' => $instance[$Model->alias][$left], $Model->escapeField($right) . ' >' => $instance[$Model->alias][$right]], 'recursive' => 0])) { + $errors[] = ['node', $instance[$Model->alias][$Model->primaryKey], 'The parent field is blank, but has a parent']; + } + } + if ($errors) { + return $errors; + } + return true; + } + + /** + * get the minimum index value in the table. + * + * @param Model $Model Model instance. + * @param string $scope Scoping conditions. + * @param string $left Left value. + * @param int $recursive Recurursive find value. + * @return int + */ + protected function _getMin(Model $Model, $scope, $left, $recursive = -1) + { + $db = ConnectionManager::getDataSource($Model->useDbConfig); + $name = $Model->escapeField($left); + list($edge) = array_values($Model->find('first', [ + 'conditions' => $scope, + 'fields' => $db->calculate($Model, 'min', [$name, $left]), + 'recursive' => $recursive, + 'order' => false, + 'callbacks' => false + ])); + return (empty($edge[$left])) ? 0 : $edge[$left]; + } + + /** + * Reorder the node without changing the parent. + * + * If the node is the last child, or is a top level node with no subsequent node this method will return false + * + * @param Model $Model Model using this behavior + * @param int|string|null $id The ID of the record to move + * @param int|bool $number how many places to move the node or true to move to last position + * @return bool true on success, false on failure + * @link https://book.cakephp.org/2.0/en/core-libraries/behaviors/tree.html#TreeBehavior::moveDown + */ + public function moveDown(Model $Model, $id = null, $number = 1) + { + if (is_array($id)) { + extract(array_merge(['id' => null], $id)); + } + if (!$number) { + return false; + } + if (empty($id)) { + $id = $Model->id; + } + extract($this->settings[$Model->alias]); + list($node) = array_values($this->_getNode($Model, $id)); + if ($node[$parent]) { + list($parentNode) = array_values($this->_getNode($Model, $node[$parent])); + if (($node[$right] + 1) == $parentNode[$right]) { + return false; + } + } + $nextNode = $Model->find('first', [ + 'conditions' => [$scope, $Model->escapeField($left) => ($node[$right] + 1)], + 'fields' => [$Model->primaryKey, $left, $right], + 'order' => false, + 'recursive' => $recursive] + ); + if ($nextNode) { + list($nextNode) = array_values($nextNode); + } else { + return false; + } + $edge = $this->_getMax($Model, $scope, $right, $recursive); + $this->_sync($Model, $edge - $node[$left] + 1, '+', 'BETWEEN ' . $node[$left] . ' AND ' . $node[$right]); + $this->_sync($Model, $nextNode[$left] - $node[$left], '-', 'BETWEEN ' . $nextNode[$left] . ' AND ' . $nextNode[$right]); + $this->_sync($Model, $edge - $node[$left] - ($nextNode[$right] - $nextNode[$left]), '-', '> ' . $edge); + + if (is_int($number)) { + $number--; + } + if ($number) { + $this->moveDown($Model, $id, $number); + } + return true; + } + + /** + * Remove the current node from the tree, and reparent all children up one level. + * + * If the parameter delete is false, the node will become a new top level node. Otherwise the node will be deleted + * after the children are reparented. + * + * @param Model $Model Model using this behavior + * @param int|string|null $id The ID of the record to remove + * @param bool $delete whether to delete the node after reparenting children (if any) + * @return bool true on success, false on failure + * @link https://book.cakephp.org/2.0/en/core-libraries/behaviors/tree.html#TreeBehavior::removeFromTree + */ + public function removeFromTree(Model $Model, $id = null, $delete = false) + { + if (is_array($id)) { + extract(array_merge(['id' => null], $id)); + } + extract($this->settings[$Model->alias]); + + list($node) = array_values($this->_getNode($Model, $id)); + + if ($node[$right] == $node[$left] + 1) { + if ($delete) { + return $Model->delete($id); + } + $Model->id = $id; + return $Model->saveField($parent, null); + } else if ($node[$parent]) { + list($parentNode) = array_values($this->_getNode($Model, $node[$parent])); + } else { + $parentNode[$right] = $node[$right] + 1; + } + + $db = ConnectionManager::getDataSource($Model->useDbConfig); + $Model->updateAll( + [$parent => $db->value($node[$parent], $parent)], + [$Model->escapeField($parent) => $node[$Model->primaryKey]] + ); + $this->_sync($Model, 1, '-', 'BETWEEN ' . ($node[$left] + 1) . ' AND ' . ($node[$right] - 1)); + $this->_sync($Model, 2, '-', '> ' . ($node[$right])); + $Model->id = $id; + + if ($delete) { + $Model->updateAll( + [ + $Model->escapeField($left) => 0, + $Model->escapeField($right) => 0, + $Model->escapeField($parent) => null + ], + [$Model->escapeField() => $id] + ); + return $Model->delete($id); + } + $edge = $this->_getMax($Model, $scope, $right, $recursive); + if ($node[$right] == $edge) { + $edge = $edge - 2; + } + $Model->id = $id; + return $Model->save( + [$left => $edge + 1, $right => $edge + 2, $parent => null], + ['callbacks' => false, 'validate' => false] + ); + } + + /** + * Returns the depth level of a node in the tree. + * + * @param Model $Model Model using this behavior + * @param int|string|null $id The primary key for record to get the level of. + * @return int|bool Integer of the level or false if the node does not exist. + */ + public function getLevel(Model $Model, $id = null) + { + if ($id === null) { + $id = $Model->id; + } + + $node = $Model->find('first', [ + 'conditions' => [$Model->escapeField() => $id], + 'order' => false, + 'recursive' => -1 + ]); + + if (empty($node)) { + return false; + } + + extract($this->settings[$Model->alias]); + + return $Model->find('count', [ + 'conditions' => [ + $scope, + $left . ' <' => $node[$Model->alias][$left], + $right . ' >' => $node[$Model->alias][$right] + ], + 'order' => false, + 'recursive' => -1 + ]); + } } diff --git a/lib/Cake/Model/BehaviorCollection.php b/lib/Cake/Model/BehaviorCollection.php index faae5706..7705946f 100755 --- a/lib/Cake/Model/BehaviorCollection.php +++ b/lib/Cake/Model/BehaviorCollection.php @@ -28,269 +28,279 @@ * * @package Cake.Model */ -class BehaviorCollection extends ObjectCollection implements CakeEventListener { +class BehaviorCollection extends ObjectCollection implements CakeEventListener +{ -/** - * Stores a reference to the attached name - * - * @var string - */ - public $modelName = null; + /** + * Stores a reference to the attached name + * + * @var string + */ + public $modelName = null; -/** - * Keeps a list of all methods of attached behaviors - * - * @var array - */ - protected $_methods = array(); + /** + * Keeps a list of all methods of attached behaviors + * + * @var array + */ + protected $_methods = []; -/** - * Keeps a list of all methods which have been mapped with regular expressions - * - * @var array - */ - protected $_mappedMethods = array(); + /** + * Keeps a list of all methods which have been mapped with regular expressions + * + * @var array + */ + protected $_mappedMethods = []; -/** - * Attaches a model object and loads a list of behaviors - * - * @param string $modelName Model name. - * @param array $behaviors Behaviors list. - * @return void - */ - public function init($modelName, $behaviors = array()) { - $this->modelName = $modelName; + /** + * Attaches a model object and loads a list of behaviors + * + * @param string $modelName Model name. + * @param array $behaviors Behaviors list. + * @return void + */ + public function init($modelName, $behaviors = []) + { + $this->modelName = $modelName; - if (!empty($behaviors)) { - foreach (BehaviorCollection::normalizeObjectArray($behaviors) as $config) { - $this->load($config['class'], $config['settings']); - } - } - } + if (!empty($behaviors)) { + foreach (BehaviorCollection::normalizeObjectArray($behaviors) as $config) { + $this->load($config['class'], $config['settings']); + } + } + } -/** - * Backwards compatible alias for load() - * - * @param string $behavior Behavior name. - * @param array $config Configuration options. - * @return bool true. - * @deprecated 3.0.0 Will be removed in 3.0. Replaced with load(). - */ - public function attach($behavior, $config = array()) { - return $this->load($behavior, $config); - } + /** + * Loads a behavior into the collection. You can use use `$config['enabled'] = false` + * to load a behavior with callbacks disabled. By default callbacks are enabled. Disable behaviors + * can still be used as normal. + * + * You can alias your behavior as an existing behavior by setting the 'className' key, i.e., + * ``` + * public $actsAs = array( + * 'Tree' => array( + * 'className' => 'AliasedTree' + * ); + * ); + * ``` + * All calls to the `Tree` behavior would use `AliasedTree` instead. + * + * @param string $behavior CamelCased name of the behavior to load + * @param array $config Behavior configuration parameters + * @return bool True on success. + * @throws MissingBehaviorException when a behavior could not be found. + */ + public function load($behavior, $config = []) + { + if (isset($config['className'])) { + $alias = $behavior; + $behavior = $config['className']; + } + $configDisabled = isset($config['enabled']) && $config['enabled'] === false; + $priority = isset($config['priority']) ? $config['priority'] : $this->defaultPriority; + unset($config['enabled'], $config['className'], $config['priority']); -/** - * Loads a behavior into the collection. You can use use `$config['enabled'] = false` - * to load a behavior with callbacks disabled. By default callbacks are enabled. Disable behaviors - * can still be used as normal. - * - * You can alias your behavior as an existing behavior by setting the 'className' key, i.e., - * ``` - * public $actsAs = array( - * 'Tree' => array( - * 'className' => 'AliasedTree' - * ); - * ); - * ``` - * All calls to the `Tree` behavior would use `AliasedTree` instead. - * - * @param string $behavior CamelCased name of the behavior to load - * @param array $config Behavior configuration parameters - * @return bool True on success. - * @throws MissingBehaviorException when a behavior could not be found. - */ - public function load($behavior, $config = array()) { - if (isset($config['className'])) { - $alias = $behavior; - $behavior = $config['className']; - } - $configDisabled = isset($config['enabled']) && $config['enabled'] === false; - $priority = isset($config['priority']) ? $config['priority'] : $this->defaultPriority; - unset($config['enabled'], $config['className'], $config['priority']); + list($plugin, $name) = pluginSplit($behavior, true); + if (!isset($alias)) { + $alias = $name; + } - list($plugin, $name) = pluginSplit($behavior, true); - if (!isset($alias)) { - $alias = $name; - } + $class = $name . 'Behavior'; - $class = $name . 'Behavior'; + App::uses($class, $plugin . 'Model/Behavior'); + if (!class_exists($class)) { + throw new MissingBehaviorException([ + 'class' => $class, + 'plugin' => substr($plugin, 0, -1) + ]); + } - App::uses($class, $plugin . 'Model/Behavior'); - if (!class_exists($class)) { - throw new MissingBehaviorException(array( - 'class' => $class, - 'plugin' => substr($plugin, 0, -1) - )); - } + if (!isset($this->{$alias})) { + if (ClassRegistry::isKeySet($class)) { + $this->_loaded[$alias] = ClassRegistry::getObject($class); + } else { + $this->_loaded[$alias] = new $class(); + ClassRegistry::addObject($class, $this->_loaded[$alias]); + } + } else if (isset($this->_loaded[$alias]->settings) && isset($this->_loaded[$alias]->settings[$this->modelName])) { + if ($config !== null && $config !== false) { + $config = array_merge($this->_loaded[$alias]->settings[$this->modelName], $config); + } else { + $config = []; + } + } + if (empty($config)) { + $config = []; + } + $this->_loaded[$alias]->settings['priority'] = $priority; + $this->_loaded[$alias]->setup(ClassRegistry::getObject($this->modelName), $config); - if (!isset($this->{$alias})) { - if (ClassRegistry::isKeySet($class)) { - $this->_loaded[$alias] = ClassRegistry::getObject($class); - } else { - $this->_loaded[$alias] = new $class(); - ClassRegistry::addObject($class, $this->_loaded[$alias]); - } - } elseif (isset($this->_loaded[$alias]->settings) && isset($this->_loaded[$alias]->settings[$this->modelName])) { - if ($config !== null && $config !== false) { - $config = array_merge($this->_loaded[$alias]->settings[$this->modelName], $config); - } else { - $config = array(); - } - } - if (empty($config)) { - $config = array(); - } - $this->_loaded[$alias]->settings['priority'] = $priority; - $this->_loaded[$alias]->setup(ClassRegistry::getObject($this->modelName), $config); + foreach ($this->_loaded[$alias]->mapMethods as $method => $methodAlias) { + $this->_mappedMethods[$method] = [$alias, $methodAlias]; + } + $methods = get_class_methods($this->_loaded[$alias]); + $parentMethods = array_flip(get_class_methods('ModelBehavior')); + $callbacks = [ + 'setup', 'cleanup', 'beforeFind', 'afterFind', 'beforeSave', 'afterSave', + 'beforeDelete', 'afterDelete', 'onError' + ]; - foreach ($this->_loaded[$alias]->mapMethods as $method => $methodAlias) { - $this->_mappedMethods[$method] = array($alias, $methodAlias); - } - $methods = get_class_methods($this->_loaded[$alias]); - $parentMethods = array_flip(get_class_methods('ModelBehavior')); - $callbacks = array( - 'setup', 'cleanup', 'beforeFind', 'afterFind', 'beforeSave', 'afterSave', - 'beforeDelete', 'afterDelete', 'onError' - ); + foreach ($methods as $m) { + if (!isset($parentMethods[$m])) { + $methodAllowed = ( + $m[0] !== '_' && !array_key_exists($m, $this->_methods) && + !in_array($m, $callbacks) + ); + if ($methodAllowed) { + $this->_methods[$m] = [$alias, $m]; + } + } + } - foreach ($methods as $m) { - if (!isset($parentMethods[$m])) { - $methodAllowed = ( - $m[0] !== '_' && !array_key_exists($m, $this->_methods) && - !in_array($m, $callbacks) - ); - if ($methodAllowed) { - $this->_methods[$m] = array($alias, $m); - } - } - } + if ($configDisabled) { + $this->disable($alias); + } else if (!$this->enabled($alias)) { + $this->enable($alias); + } else { + $this->setPriority($alias, $priority); + } - if ($configDisabled) { - $this->disable($alias); - } elseif (!$this->enabled($alias)) { - $this->enable($alias); - } else { - $this->setPriority($alias, $priority); - } + return true; + } - return true; - } + /** + * Backwards compatible alias for load() + * + * @param string $behavior Behavior name. + * @param array $config Configuration options. + * @return bool true. + * @deprecated 3.0.0 Will be removed in 3.0. Replaced with load(). + */ + public function attach($behavior, $config = []) + { + return $this->load($behavior, $config); + } -/** - * Detaches a behavior from a model - * - * @param string $name CamelCased name of the behavior to unload - * @return void - */ - public function unload($name) { - list(, $name) = pluginSplit($name); - if (isset($this->_loaded[$name])) { - $this->_loaded[$name]->cleanup(ClassRegistry::getObject($this->modelName)); - parent::unload($name); - } - foreach ($this->_methods as $m => $callback) { - if (is_array($callback) && $callback[0] === $name) { - unset($this->_methods[$m]); - } - } - } + /** + * Backwards compatible alias for unload() + * + * @param string $name Name of behavior + * @return void + * @deprecated 3.0.0 Will be removed in 3.0. Use unload instead. + */ + public function detach($name) + { + return $this->unload($name); + } -/** - * Backwards compatible alias for unload() - * - * @param string $name Name of behavior - * @return void - * @deprecated 3.0.0 Will be removed in 3.0. Use unload instead. - */ - public function detach($name) { - return $this->unload($name); - } + /** + * Detaches a behavior from a model + * + * @param string $name CamelCased name of the behavior to unload + * @return void + */ + public function unload($name) + { + list(, $name) = pluginSplit($name); + if (isset($this->_loaded[$name])) { + $this->_loaded[$name]->cleanup(ClassRegistry::getObject($this->modelName)); + parent::unload($name); + } + foreach ($this->_methods as $m => $callback) { + if (is_array($callback) && $callback[0] === $name) { + unset($this->_methods[$m]); + } + } + } -/** - * Dispatches a behavior method. Will call either normal methods or mapped methods. - * - * If a method is not handled by the BehaviorCollection, and $strict is false, a - * special return of `array('unhandled')` will be returned to signal the method was not found. - * - * @param Model $model The model the method was originally called on. - * @param string $method The method called. - * @param array $params Parameters for the called method. - * @param bool $strict If methods are not found, trigger an error. - * @return array All methods for all behaviors attached to this object - */ - public function dispatchMethod($model, $method, $params = array(), $strict = false) { - $method = $this->hasMethod($method, true); + /** + * Dispatches a behavior method. Will call either normal methods or mapped methods. + * + * If a method is not handled by the BehaviorCollection, and $strict is false, a + * special return of `array('unhandled')` will be returned to signal the method was not found. + * + * @param Model $model The model the method was originally called on. + * @param string $method The method called. + * @param array $params Parameters for the called method. + * @param bool $strict If methods are not found, trigger an error. + * @return array All methods for all behaviors attached to this object + */ + public function dispatchMethod($model, $method, $params = [], $strict = false) + { + $method = $this->hasMethod($method, true); - if ($strict && empty($method)) { - trigger_error(__d('cake_dev', '%s - Method %s not found in any attached behavior', 'BehaviorCollection::dispatchMethod()', $method), E_USER_WARNING); - return null; - } - if (empty($method)) { - return array('unhandled'); - } - if (count($method) === 3) { - array_unshift($params, $method[2]); - unset($method[2]); - } - return call_user_func_array( - array($this->_loaded[$method[0]], $method[1]), - array_merge(array(&$model), $params) - ); - } + if ($strict && empty($method)) { + trigger_error(__d('cake_dev', '%s - Method %s not found in any attached behavior', 'BehaviorCollection::dispatchMethod()', $method), E_USER_WARNING); + return null; + } + if (empty($method)) { + return ['unhandled']; + } + if (count($method) === 3) { + array_unshift($params, $method[2]); + unset($method[2]); + } + return call_user_func_array( + [$this->_loaded[$method[0]], $method[1]], + array_merge([&$model], $params) + ); + } -/** - * Gets the method list for attached behaviors, i.e. all public, non-callback methods. - * This does not include mappedMethods. - * - * @return array All public methods for all behaviors attached to this collection - */ - public function methods() { - return $this->_methods; - } + /** + * Check to see if a behavior in this collection implements the provided method. Will + * also check mappedMethods. + * + * @param string $method The method to find. + * @param bool $callback Return the callback for the method. + * @return mixed If $callback is false, a boolean will be returned, if its true, an array + * containing callback information will be returned. For mapped methods the array will have 3 elements. + */ + public function hasMethod($method, $callback = false) + { + if (isset($this->_methods[$method])) { + return $callback ? $this->_methods[$method] : true; + } + foreach ($this->_mappedMethods as $pattern => $target) { + if (preg_match($pattern . 'i', $method)) { + if ($callback) { + $target[] = $method; + return $target; + } + return true; + } + } + return false; + } -/** - * Check to see if a behavior in this collection implements the provided method. Will - * also check mappedMethods. - * - * @param string $method The method to find. - * @param bool $callback Return the callback for the method. - * @return mixed If $callback is false, a boolean will be returned, if its true, an array - * containing callback information will be returned. For mapped methods the array will have 3 elements. - */ - public function hasMethod($method, $callback = false) { - if (isset($this->_methods[$method])) { - return $callback ? $this->_methods[$method] : true; - } - foreach ($this->_mappedMethods as $pattern => $target) { - if (preg_match($pattern . 'i', $method)) { - if ($callback) { - $target[] = $method; - return $target; - } - return true; - } - } - return false; - } + /** + * Gets the method list for attached behaviors, i.e. all public, non-callback methods. + * This does not include mappedMethods. + * + * @return array All public methods for all behaviors attached to this collection + */ + public function methods() + { + return $this->_methods; + } -/** - * Returns the implemented events that will get routed to the trigger function - * in order to dispatch them separately on each behavior - * - * @return array - */ - public function implementedEvents() { - return array( - 'Model.beforeFind' => 'trigger', - 'Model.afterFind' => 'trigger', - 'Model.beforeValidate' => 'trigger', - 'Model.afterValidate' => 'trigger', - 'Model.beforeSave' => 'trigger', - 'Model.afterSave' => 'trigger', - 'Model.beforeDelete' => 'trigger', - 'Model.afterDelete' => 'trigger' - ); - } + /** + * Returns the implemented events that will get routed to the trigger function + * in order to dispatch them separately on each behavior + * + * @return array + */ + public function implementedEvents() + { + return [ + 'Model.beforeFind' => 'trigger', + 'Model.afterFind' => 'trigger', + 'Model.beforeValidate' => 'trigger', + 'Model.afterValidate' => 'trigger', + 'Model.beforeSave' => 'trigger', + 'Model.afterSave' => 'trigger', + 'Model.beforeDelete' => 'trigger', + 'Model.afterDelete' => 'trigger' + ]; + } } diff --git a/lib/Cake/Model/CakeSchema.php b/lib/Cake/Model/CakeSchema.php index f2cb4e73..d6f8fa2c 100755 --- a/lib/Cake/Model/CakeSchema.php +++ b/lib/Cake/Model/CakeSchema.php @@ -26,729 +26,746 @@ * * @package Cake.Model */ -class CakeSchema extends CakeObject { - -/** - * Name of the schema. - * - * @var string - */ - public $name = null; - -/** - * Path to write location. - * - * @var string - */ - public $path = null; - -/** - * File to write. - * - * @var string - */ - public $file = 'schema.php'; - -/** - * Connection used for read. - * - * @var string - */ - public $connection = 'default'; - -/** - * Plugin name. - * - * @var string - */ - public $plugin = null; - -/** - * Set of tables. - * - * @var array - */ - public $tables = array(); - -/** - * Constructor - * - * @param array $options Optional load object properties. - */ - public function __construct($options = array()) { - parent::__construct(); - - if (empty($options['name'])) { - $this->name = preg_replace('/schema$/i', '', get_class($this)); - } - if (!empty($options['plugin'])) { - $this->plugin = $options['plugin']; - } - - if (strtolower($this->name) === 'cake') { - $this->name = 'App'; - } - - if (empty($options['path'])) { - $this->path = CONFIG . 'Schema'; - } - - $options = array_merge(get_object_vars($this), $options); - $this->build($options); - } - -/** - * Builds schema object properties. - * - * @param array $data Loaded object properties. - * @return void - */ - public function build($data) { - $file = null; - foreach ($data as $key => $val) { - if (!empty($val)) { - if (!in_array($key, array('plugin', 'name', 'path', 'file', 'connection', 'tables', '_log'))) { - if ($key[0] === '_') { - continue; - } - $this->tables[$key] = $val; - unset($this->{$key}); - } elseif ($key !== 'tables') { - if ($key === 'name' && $val !== $this->name && !isset($data['file'])) { - $file = Inflector::underscore($val) . '.php'; - } - $this->{$key} = $val; - } - } - } - if (file_exists($this->path . DS . $file) && is_file($this->path . DS . $file)) { - $this->file = $file; - } elseif (!empty($this->plugin)) { - $this->path = CakePlugin::path($this->plugin) . 'Config' . DS . 'Schema'; - } - } - -/** - * Before callback to be implemented in subclasses. - * - * @param array $event Schema object properties. - * @return bool Should process continue. - */ - public function before($event = array()) { - return true; - } - -/** - * After callback to be implemented in subclasses. - * - * @param array $event Schema object properties. - * @return void - */ - public function after($event = array()) { - } - -/** - * Reads database and creates schema tables. - * - * @param array $options Schema object properties. - * @return array|bool Set of name and tables. - */ - public function load($options = array()) { - if (is_string($options)) { - $options = array('path' => $options); - } - - $this->build($options); - $class = $this->name . 'Schema'; - - if (!class_exists($class) && !$this->_requireFile($this->path, $this->file)) { - $class = Inflector::camelize(Inflector::slug(Configure::read('App.dir'))) . 'Schema'; - if (!class_exists($class)) { - $this->_requireFile($this->path, $this->file); - } - } - - if (class_exists($class)) { - $Schema = new $class($options); - return $Schema; - } - return false; - } - -/** - * Reads database and creates schema tables. - * - * Options - * - * - 'connection' - the db connection to use - * - 'name' - name of the schema - * - 'models' - a list of models to use, or false to ignore models - * - * @param array $options Schema object properties. - * @return array Array indexed by name and tables. - */ - public function read($options = array()) { - $options = array_merge( - array( - 'connection' => $this->connection, - 'name' => $this->name, - 'models' => true, - ), - $options - ); - $db = ConnectionManager::getDataSource($options['connection']); - - if (isset($this->plugin)) { - App::uses($this->plugin . 'AppModel', $this->plugin . '.Model'); - } - - $tables = array(); - $currentTables = (array)$db->listSources(); - - $prefix = null; - if (isset($db->config['prefix'])) { - $prefix = $db->config['prefix']; - } - - if (!is_array($options['models']) && $options['models'] !== false) { - if (isset($this->plugin)) { - $options['models'] = App::objects($this->plugin . '.Model', null, false); - } else { - $options['models'] = App::objects('Model'); - } - } - - if (is_array($options['models'])) { - foreach ($options['models'] as $model) { - $importModel = $model; - $plugin = null; - if ($model === 'AppModel') { - continue; - } - - if (isset($this->plugin)) { - if ($model === $this->plugin . 'AppModel') { - continue; - } - $importModel = $model; - $plugin = $this->plugin . '.'; - } - - App::uses($importModel, $plugin . 'Model'); - if (!class_exists($importModel)) { - continue; - } - - $vars = get_class_vars($model); - if (empty($vars['useDbConfig']) || $vars['useDbConfig'] != $options['connection']) { - continue; - } - - try { - $Object = ClassRegistry::init(array('class' => $model, 'ds' => $options['connection'])); - } catch (CakeException $e) { - continue; - } - - if (!is_object($Object) || $Object->useTable === false) { - continue; - } - $db = $Object->getDataSource(); - - $fulltable = $table = $db->fullTableName($Object, false, false); - if ($prefix && strpos($table, $prefix) !== 0) { - continue; - } - if (!in_array($fulltable, $currentTables)) { - continue; - } - - $table = $this->_noPrefixTable($prefix, $table); - - $key = array_search($fulltable, $currentTables); - if (empty($tables[$table])) { - $tables[$table] = $this->_columns($Object); - $tables[$table]['indexes'] = $db->index($Object); - $tables[$table]['tableParameters'] = $db->readTableParameters($fulltable); - unset($currentTables[$key]); - } - if (empty($Object->hasAndBelongsToMany)) { - continue; - } - foreach ($Object->hasAndBelongsToMany as $assocData) { - if (isset($assocData['with'])) { - $class = $assocData['with']; - } - if (!is_object($Object->$class)) { - continue; - } - $withTable = $db->fullTableName($Object->$class, false, false); - if ($prefix && strpos($withTable, $prefix) !== 0) { - continue; - } - if (in_array($withTable, $currentTables)) { - $key = array_search($withTable, $currentTables); - $noPrefixWith = $this->_noPrefixTable($prefix, $withTable); - - $tables[$noPrefixWith] = $this->_columns($Object->$class); - $tables[$noPrefixWith]['indexes'] = $db->index($Object->$class); - $tables[$noPrefixWith]['tableParameters'] = $db->readTableParameters($withTable); - unset($currentTables[$key]); - } - } - } - } - - if (!empty($currentTables)) { - foreach ($currentTables as $table) { - if ($prefix) { - if (strpos($table, $prefix) !== 0) { - continue; - } - $table = $this->_noPrefixTable($prefix, $table); - } - $Object = new AppModel(array( - 'name' => Inflector::classify($table), 'table' => $table, 'ds' => $options['connection'] - )); - - $systemTables = array( - 'aros', 'acos', 'aros_acos', Configure::read('Session.table'), 'i18n' - ); - - $fulltable = $db->fullTableName($Object, false, false); - - if (in_array($table, $systemTables)) { - $tables[$Object->table] = $this->_columns($Object); - $tables[$Object->table]['indexes'] = $db->index($Object); - $tables[$Object->table]['tableParameters'] = $db->readTableParameters($fulltable); - } elseif ($options['models'] === false) { - $tables[$table] = $this->_columns($Object); - $tables[$table]['indexes'] = $db->index($Object); - $tables[$table]['tableParameters'] = $db->readTableParameters($fulltable); - } else { - $tables['missing'][$table] = $this->_columns($Object); - $tables['missing'][$table]['indexes'] = $db->index($Object); - $tables['missing'][$table]['tableParameters'] = $db->readTableParameters($fulltable); - } - } - } - - ksort($tables); - return array('name' => $options['name'], 'tables' => $tables); - } - -/** - * Writes schema file from object or options. - * - * @param array|object $object Schema object or options array. - * @param array $options Schema object properties to override object. - * @return mixed False or string written to file. - */ - public function write($object, $options = array()) { - if (is_object($object)) { - $object = get_object_vars($object); - $this->build($object); - } - - if (is_array($object)) { - $options = $object; - unset($object); - } - - $options = array_merge( - get_object_vars($this), $options - ); - - $out = "class {$options['name']}Schema extends CakeSchema {\n\n"; - - if ($options['path'] !== $this->path) { - $out .= "\tpublic \$path = '{$options['path']}';\n\n"; - } - - if ($options['file'] !== $this->file) { - $out .= "\tpublic \$file = '{$options['file']}';\n\n"; - } - - if ($options['connection'] !== 'default') { - $out .= "\tpublic \$connection = '{$options['connection']}';\n\n"; - } - - $out .= "\tpublic function before(\$event = array()) {\n\t\treturn true;\n\t}\n\n\tpublic function after(\$event = array()) {\n\t}\n\n"; - - if (empty($options['tables'])) { - $this->read(); - } - - foreach ($options['tables'] as $table => $fields) { - if (!is_numeric($table) && $table !== 'missing') { - $out .= $this->generateTable($table, $fields); - } - } - $out .= "}\n"; - - $file = new File($options['path'] . DS . $options['file'], true); - $content = "write($content)) { - return $content; - } - return false; - } - -/** - * Generate the schema code for a table. - * - * Takes a table name and $fields array and returns a completed, - * escaped variable declaration to be used in schema classes. - * - * @param string $table Table name you want returned. - * @param array $fields Array of field information to generate the table with. - * @return string Variable declaration for a schema class. - * @throws Exception - */ - public function generateTable($table, $fields) { - // Valid var name regex (http://www.php.net/manual/en/language.variables.basics.php) - if (!preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/', $table)) { - throw new Exception("Invalid table name '{$table}'"); - } - - $out = "\tpublic \${$table} = array(\n"; - if (is_array($fields)) { - $cols = array(); - foreach ($fields as $field => $value) { - if ($field !== 'indexes' && $field !== 'tableParameters') { - if (is_string($value)) { - $type = $value; - $value = array('type' => $type); - } - $value['type'] = addslashes($value['type']); - $col = "\t\t'{$field}' => array('type' => '" . $value['type'] . "', "; - unset($value['type']); - $col .= implode(', ', $this->_values($value)); - } elseif ($field === 'indexes') { - $col = "\t\t'indexes' => array(\n\t\t\t"; - $props = array(); - foreach ((array)$value as $key => $index) { - $props[] = "'{$key}' => array(" . implode(', ', $this->_values($index)) . ")"; - } - $col .= implode(",\n\t\t\t", $props) . "\n\t\t"; - } elseif ($field === 'tableParameters') { - $col = "\t\t'tableParameters' => array("; - $props = $this->_values($value); - $col .= implode(', ', $props); - } - $col .= ")"; - $cols[] = $col; - } - $out .= implode(",\n", $cols); - } - $out .= "\n\t);\n\n"; - return $out; - } - -/** - * Compares two sets of schemas. - * - * @param array|object $old Schema object or array. - * @param array|object $new Schema object or array. - * @return array Tables (that are added, dropped, or changed.) - */ - public function compare($old, $new = null) { - if (empty($new)) { - $new = $this; - } - if (is_array($new)) { - if (isset($new['tables'])) { - $new = $new['tables']; - } - } else { - $new = $new->tables; - } - - if (is_array($old)) { - if (isset($old['tables'])) { - $old = $old['tables']; - } - } else { - $old = $old->tables; - } - $tables = array(); - foreach ($new as $table => $fields) { - if ($table === 'missing') { - continue; - } - if (!array_key_exists($table, $old)) { - $tables[$table]['create'] = $fields; - } else { - $diff = $this->_arrayDiffAssoc($fields, $old[$table]); - if (!empty($diff)) { - $tables[$table]['add'] = $diff; - } - $diff = $this->_arrayDiffAssoc($old[$table], $fields); - if (!empty($diff)) { - $tables[$table]['drop'] = $diff; - } - } - - foreach ($fields as $field => $value) { - if (!empty($old[$table][$field])) { - $diff = $this->_arrayDiffAssoc($value, $old[$table][$field]); - if (empty($diff)) { - $diff = $this->_arrayDiffAssoc($old[$table][$field], $value); - } - if (!empty($diff) && $field !== 'indexes' && $field !== 'tableParameters') { - $tables[$table]['change'][$field] = $value; - } - } - - if (isset($tables[$table]['add'][$field]) && $field !== 'indexes' && $field !== 'tableParameters') { - $wrapper = array_keys($fields); - if ($column = array_search($field, $wrapper)) { - if (isset($wrapper[$column - 1])) { - $tables[$table]['add'][$field]['after'] = $wrapper[$column - 1]; - } - } - } - } - - if (isset($old[$table]['indexes']) && isset($new[$table]['indexes'])) { - $diff = $this->_compareIndexes($new[$table]['indexes'], $old[$table]['indexes']); - if ($diff) { - if (!isset($tables[$table])) { - $tables[$table] = array(); - } - if (isset($diff['drop'])) { - $tables[$table]['drop']['indexes'] = $diff['drop']; - } - if ($diff && isset($diff['add'])) { - $tables[$table]['add']['indexes'] = $diff['add']; - } - } - } - if (isset($old[$table]['tableParameters']) && isset($new[$table]['tableParameters'])) { - $diff = $this->_compareTableParameters($new[$table]['tableParameters'], $old[$table]['tableParameters']); - if ($diff) { - $tables[$table]['change']['tableParameters'] = $diff; - } - } - } - return $tables; - } - -/** - * Extended array_diff_assoc noticing change from/to NULL values. - * - * It behaves almost the same way as array_diff_assoc except for NULL values: if - * one of the values is not NULL - change is detected. It is useful in situation - * where one value is strval('') ant other is strval(null) - in string comparing - * methods this results as EQUAL, while it is not. - * - * @param array $array1 Base array. - * @param array $array2 Corresponding array checked for equality. - * @return array Difference as array with array(keys => values) from input array - * where match was not found. - */ - protected function _arrayDiffAssoc($array1, $array2) { - $difference = array(); - foreach ($array1 as $key => $value) { - if (!array_key_exists($key, $array2)) { - $difference[$key] = $value; - continue; - } - $correspondingValue = $array2[$key]; - if (($value === null) !== ($correspondingValue === null)) { - $difference[$key] = $value; - continue; - } - if (is_bool($value) !== is_bool($correspondingValue)) { - $difference[$key] = $value; - continue; - } - if (is_array($value) && is_array($correspondingValue)) { - continue; - } - if ($value === $correspondingValue) { - continue; - } - $difference[$key] = $value; - } - return $difference; - } - -/** - * Formats Schema columns from Model Object. - * - * @param array $values Options keys(type, null, default, key, length, extra). - * @return array Formatted values. - */ - protected function _values($values) { - $vals = array(); - if (is_array($values)) { - foreach ($values as $key => $val) { - if (is_array($val)) { - $vals[] = "'{$key}' => array(" . implode(", ", $this->_values($val)) . ")"; - } else { - $val = var_export($val, true); - if ($val === 'NULL') { - $val = 'null'; - } - if (!is_numeric($key)) { - $vals[] = "'{$key}' => {$val}"; - } else { - $vals[] = "{$val}"; - } - } - } - } - return $vals; - } - -/** - * Formats Schema columns from Model Object. - * - * @param array &$Obj model object. - * @return array Formatted columns. - */ - protected function _columns(&$Obj) { - $db = $Obj->getDataSource(); - $fields = $Obj->schema(true); - - $hasPrimaryAlready = false; - foreach ($fields as $value) { - if (isset($value['key']) && $value['key'] === 'primary') { - $hasPrimaryAlready = true; - break; - } - } - - $columns = array(); - foreach ($fields as $name => $value) { - if ($Obj->primaryKey === $name && !$hasPrimaryAlready && !isset($value['key'])) { - $value['key'] = 'primary'; - } - if (substr($value['type'], 0, 4) !== 'enum') { - if (!isset($db->columns[$value['type']])) { - trigger_error(__d('cake_dev', 'Schema generation error: invalid column type %s for %s.%s does not exist in DBO', $value['type'], $Obj->name, $name), E_USER_NOTICE); - continue; - } else { - $defaultCol = $db->columns[$value['type']]; - if (isset($defaultCol['limit']) && $defaultCol['limit'] == $value['length']) { - unset($value['length']); - } elseif (isset($defaultCol['length']) && $defaultCol['length'] == $value['length']) { - unset($value['length']); - } - unset($value['limit']); - } - } - - if (isset($value['default']) && ($value['default'] === '' || ($value['default'] === false && $value['type'] !== 'boolean'))) { - unset($value['default']); - } - if (empty($value['length'])) { - unset($value['length']); - } - if (empty($value['key'])) { - unset($value['key']); - } - $columns[$name] = $value; - } - - return $columns; - } - -/** - * Compare two schema files table Parameters. - * - * @param array $new New indexes. - * @param array $old Old indexes. - * @return mixed False on failure, or an array of parameters to add & drop. - */ - protected function _compareTableParameters($new, $old) { - if (!is_array($new) || !is_array($old)) { - return false; - } - $change = $this->_arrayDiffAssoc($new, $old); - return $change; - } - -/** - * Compare two schema indexes. - * - * @param array $new New indexes. - * @param array $old Old indexes. - * @return mixed False on failure or array of indexes to add and drop. - */ - protected function _compareIndexes($new, $old) { - if (!is_array($new) || !is_array($old)) { - return false; - } - - $add = $drop = array(); - - $diff = $this->_arrayDiffAssoc($new, $old); - if (!empty($diff)) { - $add = $diff; - } - - $diff = $this->_arrayDiffAssoc($old, $new); - if (!empty($diff)) { - $drop = $diff; - } - - foreach ($new as $name => $value) { - if (isset($old[$name])) { - $newUnique = isset($value['unique']) ? $value['unique'] : 0; - $oldUnique = isset($old[$name]['unique']) ? $old[$name]['unique'] : 0; - $newColumn = $value['column']; - $oldColumn = $old[$name]['column']; - - $diff = false; - - if ($newUnique != $oldUnique) { - $diff = true; - } elseif (is_array($newColumn) && is_array($oldColumn)) { - $diff = ($newColumn !== $oldColumn); - } elseif (is_string($newColumn) && is_string($oldColumn)) { - $diff = ($newColumn != $oldColumn); - } else { - $diff = true; - } - if ($diff) { - $drop[$name] = null; - $add[$name] = $value; - } - } - } - return array_filter(compact('add', 'drop')); - } - -/** - * Trim the table prefix from the full table name, and return the prefix-less - * table. - * - * @param string $prefix Table prefix. - * @param string $table Full table name. - * @return string Prefix-less table name. - */ - protected function _noPrefixTable($prefix, $table) { - return preg_replace('/^' . preg_quote($prefix) . '/', '', $table); - } - -/** - * Attempts to require the schema file specified. - * - * @param string $path Filesystem path to the file. - * @param string $file Filesystem basename of the file. - * @return bool True when a file was successfully included, false on failure. - */ - protected function _requireFile($path, $file) { - if (file_exists($path . DS . $file) && is_file($path . DS . $file)) { - require_once $path . DS . $file; - return true; - } elseif (file_exists($path . DS . 'schema.php') && is_file($path . DS . 'schema.php')) { - require_once $path . DS . 'schema.php'; - return true; - } - return false; - } +class CakeSchema extends CakeObject +{ + + /** + * Name of the schema. + * + * @var string + */ + public $name = null; + + /** + * Path to write location. + * + * @var string + */ + public $path = null; + + /** + * File to write. + * + * @var string + */ + public $file = 'schema.php'; + + /** + * Connection used for read. + * + * @var string + */ + public $connection = 'default'; + + /** + * Plugin name. + * + * @var string + */ + public $plugin = null; + + /** + * Set of tables. + * + * @var array + */ + public $tables = []; + + /** + * Constructor + * + * @param array $options Optional load object properties. + */ + public function __construct($options = []) + { + parent::__construct(); + + if (empty($options['name'])) { + $this->name = preg_replace('/schema$/i', '', get_class($this)); + } + if (!empty($options['plugin'])) { + $this->plugin = $options['plugin']; + } + + if (strtolower($this->name) === 'cake') { + $this->name = 'App'; + } + + if (empty($options['path'])) { + $this->path = CONFIG . 'Schema'; + } + + $options = array_merge(get_object_vars($this), $options); + $this->build($options); + } + + /** + * Builds schema object properties. + * + * @param array $data Loaded object properties. + * @return void + */ + public function build($data) + { + $file = null; + foreach ($data as $key => $val) { + if (!empty($val)) { + if (!in_array($key, ['plugin', 'name', 'path', 'file', 'connection', 'tables', '_log'])) { + if ($key[0] === '_') { + continue; + } + $this->tables[$key] = $val; + unset($this->{$key}); + } else if ($key !== 'tables') { + if ($key === 'name' && $val !== $this->name && !isset($data['file'])) { + $file = Inflector::underscore($val) . '.php'; + } + $this->{$key} = $val; + } + } + } + if (file_exists($this->path . DS . $file) && is_file($this->path . DS . $file)) { + $this->file = $file; + } else if (!empty($this->plugin)) { + $this->path = CakePlugin::path($this->plugin) . 'Config' . DS . 'Schema'; + } + } + + /** + * Before callback to be implemented in subclasses. + * + * @param array $event Schema object properties. + * @return bool Should process continue. + */ + public function before($event = []) + { + return true; + } + + /** + * After callback to be implemented in subclasses. + * + * @param array $event Schema object properties. + * @return void + */ + public function after($event = []) + { + } + + /** + * Reads database and creates schema tables. + * + * @param array $options Schema object properties. + * @return array|bool Set of name and tables. + */ + public function load($options = []) + { + if (is_string($options)) { + $options = ['path' => $options]; + } + + $this->build($options); + $class = $this->name . 'Schema'; + + if (!class_exists($class) && !$this->_requireFile($this->path, $this->file)) { + $class = Inflector::camelize(Inflector::slug(Configure::read('App.dir'))) . 'Schema'; + if (!class_exists($class)) { + $this->_requireFile($this->path, $this->file); + } + } + + if (class_exists($class)) { + $Schema = new $class($options); + return $Schema; + } + return false; + } + + /** + * Attempts to require the schema file specified. + * + * @param string $path Filesystem path to the file. + * @param string $file Filesystem basename of the file. + * @return bool True when a file was successfully included, false on failure. + */ + protected function _requireFile($path, $file) + { + if (file_exists($path . DS . $file) && is_file($path . DS . $file)) { + require_once $path . DS . $file; + return true; + } else if (file_exists($path . DS . 'schema.php') && is_file($path . DS . 'schema.php')) { + require_once $path . DS . 'schema.php'; + return true; + } + return false; + } + + /** + * Writes schema file from object or options. + * + * @param array|object $object Schema object or options array. + * @param array $options Schema object properties to override object. + * @return mixed False or string written to file. + */ + public function write($object, $options = []) + { + if (is_object($object)) { + $object = get_object_vars($object); + $this->build($object); + } + + if (is_array($object)) { + $options = $object; + unset($object); + } + + $options = array_merge( + get_object_vars($this), $options + ); + + $out = "class {$options['name']}Schema extends CakeSchema {\n\n"; + + if ($options['path'] !== $this->path) { + $out .= "\tpublic \$path = '{$options['path']}';\n\n"; + } + + if ($options['file'] !== $this->file) { + $out .= "\tpublic \$file = '{$options['file']}';\n\n"; + } + + if ($options['connection'] !== 'default') { + $out .= "\tpublic \$connection = '{$options['connection']}';\n\n"; + } + + $out .= "\tpublic function before(\$event = array()) {\n\t\treturn true;\n\t}\n\n\tpublic function after(\$event = array()) {\n\t}\n\n"; + + if (empty($options['tables'])) { + $this->read(); + } + + foreach ($options['tables'] as $table => $fields) { + if (!is_numeric($table) && $table !== 'missing') { + $out .= $this->generateTable($table, $fields); + } + } + $out .= "}\n"; + + $file = new File($options['path'] . DS . $options['file'], true); + $content = "write($content)) { + return $content; + } + return false; + } + + /** + * Reads database and creates schema tables. + * + * Options + * + * - 'connection' - the db connection to use + * - 'name' - name of the schema + * - 'models' - a list of models to use, or false to ignore models + * + * @param array $options Schema object properties. + * @return array Array indexed by name and tables. + */ + public function read($options = []) + { + $options = array_merge( + [ + 'connection' => $this->connection, + 'name' => $this->name, + 'models' => true, + ], + $options + ); + $db = ConnectionManager::getDataSource($options['connection']); + + if (isset($this->plugin)) { + App::uses($this->plugin . 'AppModel', $this->plugin . '.Model'); + } + + $tables = []; + $currentTables = (array)$db->listSources(); + + $prefix = null; + if (isset($db->config['prefix'])) { + $prefix = $db->config['prefix']; + } + + if (!is_array($options['models']) && $options['models'] !== false) { + if (isset($this->plugin)) { + $options['models'] = App::objects($this->plugin . '.Model', null, false); + } else { + $options['models'] = App::objects('Model'); + } + } + + if (is_array($options['models'])) { + foreach ($options['models'] as $model) { + $importModel = $model; + $plugin = null; + if ($model === 'AppModel') { + continue; + } + + if (isset($this->plugin)) { + if ($model === $this->plugin . 'AppModel') { + continue; + } + $importModel = $model; + $plugin = $this->plugin . '.'; + } + + App::uses($importModel, $plugin . 'Model'); + if (!class_exists($importModel)) { + continue; + } + + $vars = get_class_vars($model); + if (empty($vars['useDbConfig']) || $vars['useDbConfig'] != $options['connection']) { + continue; + } + + try { + $Object = ClassRegistry::init(['class' => $model, 'ds' => $options['connection']]); + } catch (CakeException $e) { + continue; + } + + if (!is_object($Object) || $Object->useTable === false) { + continue; + } + $db = $Object->getDataSource(); + + $fulltable = $table = $db->fullTableName($Object, false, false); + if ($prefix && strpos($table, $prefix) !== 0) { + continue; + } + if (!in_array($fulltable, $currentTables)) { + continue; + } + + $table = $this->_noPrefixTable($prefix, $table); + + $key = array_search($fulltable, $currentTables); + if (empty($tables[$table])) { + $tables[$table] = $this->_columns($Object); + $tables[$table]['indexes'] = $db->index($Object); + $tables[$table]['tableParameters'] = $db->readTableParameters($fulltable); + unset($currentTables[$key]); + } + if (empty($Object->hasAndBelongsToMany)) { + continue; + } + foreach ($Object->hasAndBelongsToMany as $assocData) { + if (isset($assocData['with'])) { + $class = $assocData['with']; + } + if (!is_object($Object->$class)) { + continue; + } + $withTable = $db->fullTableName($Object->$class, false, false); + if ($prefix && strpos($withTable, $prefix) !== 0) { + continue; + } + if (in_array($withTable, $currentTables)) { + $key = array_search($withTable, $currentTables); + $noPrefixWith = $this->_noPrefixTable($prefix, $withTable); + + $tables[$noPrefixWith] = $this->_columns($Object->$class); + $tables[$noPrefixWith]['indexes'] = $db->index($Object->$class); + $tables[$noPrefixWith]['tableParameters'] = $db->readTableParameters($withTable); + unset($currentTables[$key]); + } + } + } + } + + if (!empty($currentTables)) { + foreach ($currentTables as $table) { + if ($prefix) { + if (strpos($table, $prefix) !== 0) { + continue; + } + $table = $this->_noPrefixTable($prefix, $table); + } + $Object = new AppModel([ + 'name' => Inflector::classify($table), 'table' => $table, 'ds' => $options['connection'] + ]); + + $systemTables = [ + 'aros', 'acos', 'aros_acos', Configure::read('Session.table'), 'i18n' + ]; + + $fulltable = $db->fullTableName($Object, false, false); + + if (in_array($table, $systemTables)) { + $tables[$Object->table] = $this->_columns($Object); + $tables[$Object->table]['indexes'] = $db->index($Object); + $tables[$Object->table]['tableParameters'] = $db->readTableParameters($fulltable); + } else if ($options['models'] === false) { + $tables[$table] = $this->_columns($Object); + $tables[$table]['indexes'] = $db->index($Object); + $tables[$table]['tableParameters'] = $db->readTableParameters($fulltable); + } else { + $tables['missing'][$table] = $this->_columns($Object); + $tables['missing'][$table]['indexes'] = $db->index($Object); + $tables['missing'][$table]['tableParameters'] = $db->readTableParameters($fulltable); + } + } + } + + ksort($tables); + return ['name' => $options['name'], 'tables' => $tables]; + } + + /** + * Trim the table prefix from the full table name, and return the prefix-less + * table. + * + * @param string $prefix Table prefix. + * @param string $table Full table name. + * @return string Prefix-less table name. + */ + protected function _noPrefixTable($prefix, $table) + { + return preg_replace('/^' . preg_quote($prefix) . '/', '', $table); + } + + /** + * Formats Schema columns from Model Object. + * + * @param array &$Obj model object. + * @return array Formatted columns. + */ + protected function _columns(&$Obj) + { + $db = $Obj->getDataSource(); + $fields = $Obj->schema(true); + + $hasPrimaryAlready = false; + foreach ($fields as $value) { + if (isset($value['key']) && $value['key'] === 'primary') { + $hasPrimaryAlready = true; + break; + } + } + + $columns = []; + foreach ($fields as $name => $value) { + if ($Obj->primaryKey === $name && !$hasPrimaryAlready && !isset($value['key'])) { + $value['key'] = 'primary'; + } + if (substr($value['type'], 0, 4) !== 'enum') { + if (!isset($db->columns[$value['type']])) { + trigger_error(__d('cake_dev', 'Schema generation error: invalid column type %s for %s.%s does not exist in DBO', $value['type'], $Obj->name, $name), E_USER_NOTICE); + continue; + } else { + $defaultCol = $db->columns[$value['type']]; + if (isset($defaultCol['limit']) && $defaultCol['limit'] == $value['length']) { + unset($value['length']); + } else if (isset($defaultCol['length']) && $defaultCol['length'] == $value['length']) { + unset($value['length']); + } + unset($value['limit']); + } + } + + if (isset($value['default']) && ($value['default'] === '' || ($value['default'] === false && $value['type'] !== 'boolean'))) { + unset($value['default']); + } + if (empty($value['length'])) { + unset($value['length']); + } + if (empty($value['key'])) { + unset($value['key']); + } + $columns[$name] = $value; + } + + return $columns; + } + + /** + * Generate the schema code for a table. + * + * Takes a table name and $fields array and returns a completed, + * escaped variable declaration to be used in schema classes. + * + * @param string $table Table name you want returned. + * @param array $fields Array of field information to generate the table with. + * @return string Variable declaration for a schema class. + * @throws Exception + */ + public function generateTable($table, $fields) + { + // Valid var name regex (http://www.php.net/manual/en/language.variables.basics.php) + if (!preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/', $table)) { + throw new Exception("Invalid table name '{$table}'"); + } + + $out = "\tpublic \${$table} = array(\n"; + if (is_array($fields)) { + $cols = []; + foreach ($fields as $field => $value) { + if ($field !== 'indexes' && $field !== 'tableParameters') { + if (is_string($value)) { + $type = $value; + $value = ['type' => $type]; + } + $value['type'] = addslashes($value['type']); + $col = "\t\t'{$field}' => array('type' => '" . $value['type'] . "', "; + unset($value['type']); + $col .= implode(', ', $this->_values($value)); + } else if ($field === 'indexes') { + $col = "\t\t'indexes' => array(\n\t\t\t"; + $props = []; + foreach ((array)$value as $key => $index) { + $props[] = "'{$key}' => array(" . implode(', ', $this->_values($index)) . ")"; + } + $col .= implode(",\n\t\t\t", $props) . "\n\t\t"; + } else if ($field === 'tableParameters') { + $col = "\t\t'tableParameters' => array("; + $props = $this->_values($value); + $col .= implode(', ', $props); + } + $col .= ")"; + $cols[] = $col; + } + $out .= implode(",\n", $cols); + } + $out .= "\n\t);\n\n"; + return $out; + } + + /** + * Formats Schema columns from Model Object. + * + * @param array $values Options keys(type, null, default, key, length, extra). + * @return array Formatted values. + */ + protected function _values($values) + { + $vals = []; + if (is_array($values)) { + foreach ($values as $key => $val) { + if (is_array($val)) { + $vals[] = "'{$key}' => array(" . implode(", ", $this->_values($val)) . ")"; + } else { + $val = var_export($val, true); + if ($val === 'NULL') { + $val = 'null'; + } + if (!is_numeric($key)) { + $vals[] = "'{$key}' => {$val}"; + } else { + $vals[] = "{$val}"; + } + } + } + } + return $vals; + } + + /** + * Compares two sets of schemas. + * + * @param array|object $old Schema object or array. + * @param array|object $new Schema object or array. + * @return array Tables (that are added, dropped, or changed.) + */ + public function compare($old, $new = null) + { + if (empty($new)) { + $new = $this; + } + if (is_array($new)) { + if (isset($new['tables'])) { + $new = $new['tables']; + } + } else { + $new = $new->tables; + } + + if (is_array($old)) { + if (isset($old['tables'])) { + $old = $old['tables']; + } + } else { + $old = $old->tables; + } + $tables = []; + foreach ($new as $table => $fields) { + if ($table === 'missing') { + continue; + } + if (!array_key_exists($table, $old)) { + $tables[$table]['create'] = $fields; + } else { + $diff = $this->_arrayDiffAssoc($fields, $old[$table]); + if (!empty($diff)) { + $tables[$table]['add'] = $diff; + } + $diff = $this->_arrayDiffAssoc($old[$table], $fields); + if (!empty($diff)) { + $tables[$table]['drop'] = $diff; + } + } + + foreach ($fields as $field => $value) { + if (!empty($old[$table][$field])) { + $diff = $this->_arrayDiffAssoc($value, $old[$table][$field]); + if (empty($diff)) { + $diff = $this->_arrayDiffAssoc($old[$table][$field], $value); + } + if (!empty($diff) && $field !== 'indexes' && $field !== 'tableParameters') { + $tables[$table]['change'][$field] = $value; + } + } + + if (isset($tables[$table]['add'][$field]) && $field !== 'indexes' && $field !== 'tableParameters') { + $wrapper = array_keys($fields); + if ($column = array_search($field, $wrapper)) { + if (isset($wrapper[$column - 1])) { + $tables[$table]['add'][$field]['after'] = $wrapper[$column - 1]; + } + } + } + } + + if (isset($old[$table]['indexes']) && isset($new[$table]['indexes'])) { + $diff = $this->_compareIndexes($new[$table]['indexes'], $old[$table]['indexes']); + if ($diff) { + if (!isset($tables[$table])) { + $tables[$table] = []; + } + if (isset($diff['drop'])) { + $tables[$table]['drop']['indexes'] = $diff['drop']; + } + if ($diff && isset($diff['add'])) { + $tables[$table]['add']['indexes'] = $diff['add']; + } + } + } + if (isset($old[$table]['tableParameters']) && isset($new[$table]['tableParameters'])) { + $diff = $this->_compareTableParameters($new[$table]['tableParameters'], $old[$table]['tableParameters']); + if ($diff) { + $tables[$table]['change']['tableParameters'] = $diff; + } + } + } + return $tables; + } + + /** + * Extended array_diff_assoc noticing change from/to NULL values. + * + * It behaves almost the same way as array_diff_assoc except for NULL values: if + * one of the values is not NULL - change is detected. It is useful in situation + * where one value is strval('') ant other is strval(null) - in string comparing + * methods this results as EQUAL, while it is not. + * + * @param array $array1 Base array. + * @param array $array2 Corresponding array checked for equality. + * @return array Difference as array with array(keys => values) from input array + * where match was not found. + */ + protected function _arrayDiffAssoc($array1, $array2) + { + $difference = []; + foreach ($array1 as $key => $value) { + if (!array_key_exists($key, $array2)) { + $difference[$key] = $value; + continue; + } + $correspondingValue = $array2[$key]; + if (($value === null) !== ($correspondingValue === null)) { + $difference[$key] = $value; + continue; + } + if (is_bool($value) !== is_bool($correspondingValue)) { + $difference[$key] = $value; + continue; + } + if (is_array($value) && is_array($correspondingValue)) { + continue; + } + if ($value === $correspondingValue) { + continue; + } + $difference[$key] = $value; + } + return $difference; + } + + /** + * Compare two schema indexes. + * + * @param array $new New indexes. + * @param array $old Old indexes. + * @return mixed False on failure or array of indexes to add and drop. + */ + protected function _compareIndexes($new, $old) + { + if (!is_array($new) || !is_array($old)) { + return false; + } + + $add = $drop = []; + + $diff = $this->_arrayDiffAssoc($new, $old); + if (!empty($diff)) { + $add = $diff; + } + + $diff = $this->_arrayDiffAssoc($old, $new); + if (!empty($diff)) { + $drop = $diff; + } + + foreach ($new as $name => $value) { + if (isset($old[$name])) { + $newUnique = isset($value['unique']) ? $value['unique'] : 0; + $oldUnique = isset($old[$name]['unique']) ? $old[$name]['unique'] : 0; + $newColumn = $value['column']; + $oldColumn = $old[$name]['column']; + + $diff = false; + + if ($newUnique != $oldUnique) { + $diff = true; + } else if (is_array($newColumn) && is_array($oldColumn)) { + $diff = ($newColumn !== $oldColumn); + } else if (is_string($newColumn) && is_string($oldColumn)) { + $diff = ($newColumn != $oldColumn); + } else { + $diff = true; + } + if ($diff) { + $drop[$name] = null; + $add[$name] = $value; + } + } + } + return array_filter(compact('add', 'drop')); + } + + /** + * Compare two schema files table Parameters. + * + * @param array $new New indexes. + * @param array $old Old indexes. + * @return mixed False on failure, or an array of parameters to add & drop. + */ + protected function _compareTableParameters($new, $old) + { + if (!is_array($new) || !is_array($old)) { + return false; + } + $change = $this->_arrayDiffAssoc($new, $old); + return $change; + } } diff --git a/lib/Cake/Model/ConnectionManager.php b/lib/Cake/Model/ConnectionManager.php index 2a93624c..ae52422a 100755 --- a/lib/Cake/Model/ConnectionManager.php +++ b/lib/Cake/Model/ConnectionManager.php @@ -28,243 +28,254 @@ * * @package Cake.Model */ -class ConnectionManager { +class ConnectionManager +{ -/** - * Holds a loaded instance of the Connections object - * - * @var DATABASE_CONFIG - */ - public static $config = null; + /** + * Holds a loaded instance of the Connections object + * + * @var DATABASE_CONFIG + */ + public static $config = null; -/** - * Holds instances DataSource objects - * - * @var array - */ - protected static $_dataSources = array(); + /** + * Holds instances DataSource objects + * + * @var array + */ + protected static $_dataSources = []; -/** - * Contains a list of all file and class names used in Connection settings - * - * @var array - */ - protected static $_connectionsEnum = array(); + /** + * Contains a list of all file and class names used in Connection settings + * + * @var array + */ + protected static $_connectionsEnum = []; -/** - * Indicates if the init code for this class has already been executed - * - * @var bool - */ - protected static $_init = false; + /** + * Indicates if the init code for this class has already been executed + * + * @var bool + */ + protected static $_init = false; -/** - * Loads connections configuration. - * - * @return void - */ - protected static function _init() { - include_once CONFIG . 'database.php'; - if (class_exists('DATABASE_CONFIG')) { - static::$config = new DATABASE_CONFIG(); - } - static::$_init = true; - } + /** + * Gets the list of available DataSource connections + * This will only return the datasources instantiated by this manager + * It differs from enumConnectionObjects, since the latter will return all configured connections + * + * @return array List of available connections + */ + public static function sourceList() + { + if (empty(static::$_init)) { + static::_init(); + } + return array_keys(static::$_dataSources); + } -/** - * Gets a reference to a DataSource object - * - * @param string $name The name of the DataSource, as defined in app/Config/database.php - * @return DataSource Instance - * @throws MissingDatasourceException - */ - public static function getDataSource($name) { - if (empty(static::$_init)) { - static::_init(); - } + /** + * Loads connections configuration. + * + * @return void + */ + protected static function _init() + { + include_once CONFIG . 'database.php'; + if (class_exists('DATABASE_CONFIG')) { + static::$config = new DATABASE_CONFIG(); + } + static::$_init = true; + } - if (!empty(static::$_dataSources[$name])) { - return static::$_dataSources[$name]; - } + /** + * Gets a DataSource name from an object reference. + * + * @param DataSource $source DataSource object + * @return string|null Datasource name, or null if source is not present + * in the ConnectionManager. + */ + public static function getSourceName($source) + { + if (empty(static::$_init)) { + static::_init(); + } + foreach (static::$_dataSources as $name => $ds) { + if ($ds === $source) { + return $name; + } + } + return null; + } - if (empty(static::$_connectionsEnum[$name])) { - static::_getConnectionObject($name); - } + /** + * Returns a list of connections + * + * @return array An associative array of elements where the key is the connection name + * (as defined in Connections), and the value is an array with keys 'filename' and 'classname'. + */ + public static function enumConnectionObjects() + { + if (empty(static::$_init)) { + static::_init(); + } + return (array)static::$config; + } - static::loadDataSource($name); - $conn = static::$_connectionsEnum[$name]; - $class = $conn['classname']; + /** + * Dynamically creates a DataSource object at runtime, with the given name and settings + * + * @param string $name The DataSource name + * @param array $config The DataSource configuration settings + * @return DataSource|null A reference to the DataSource object, or null if creation failed + */ + public static function create($name = '', $config = []) + { + if (empty(static::$_init)) { + static::_init(); + } - if (strpos(App::location($class), 'Datasource') === false) { - throw new MissingDatasourceException(array( - 'class' => $class, - 'plugin' => null, - 'message' => 'Datasource is not found in Model/Datasource package.' - )); - } - static::$_dataSources[$name] = new $class(static::$config->{$name}); - static::$_dataSources[$name]->configKeyName = $name; + if (empty($name) || empty($config) || array_key_exists($name, static::$_connectionsEnum)) { + return null; + } + static::$config->{$name} = $config; + static::$_connectionsEnum[$name] = static::_connectionData($config); + $return = static::getDataSource($name); + return $return; + } - return static::$_dataSources[$name]; - } + /** + * Returns the file, class name, and parent for the given driver. + * + * @param array $config Array with connection configuration. Key 'datasource' is required + * @return array An indexed array with: filename, classname, plugin and parent + */ + protected static function _connectionData($config) + { + $package = $classname = $plugin = null; -/** - * Gets the list of available DataSource connections - * This will only return the datasources instantiated by this manager - * It differs from enumConnectionObjects, since the latter will return all configured connections - * - * @return array List of available connections - */ - public static function sourceList() { - if (empty(static::$_init)) { - static::_init(); - } - return array_keys(static::$_dataSources); - } + list($plugin, $classname) = pluginSplit($config['datasource']); + if (strpos($classname, '/') !== false) { + $package = dirname($classname); + $classname = basename($classname); + } + return compact('package', 'classname', 'plugin'); + } -/** - * Gets a DataSource name from an object reference. - * - * @param DataSource $source DataSource object - * @return string|null Datasource name, or null if source is not present - * in the ConnectionManager. - */ - public static function getSourceName($source) { - if (empty(static::$_init)) { - static::_init(); - } - foreach (static::$_dataSources as $name => $ds) { - if ($ds === $source) { - return $name; - } - } - return null; - } + /** + * Gets a reference to a DataSource object + * + * @param string $name The name of the DataSource, as defined in app/Config/database.php + * @return DataSource Instance + * @throws MissingDatasourceException + */ + public static function getDataSource($name) + { + if (empty(static::$_init)) { + static::_init(); + } -/** - * Loads the DataSource class for the given connection name - * - * @param string|array $connName A string name of the connection, as defined in app/Config/database.php, - * or an array containing the filename (without extension) and class name of the object, - * to be found in app/Model/Datasource/ or lib/Cake/Model/Datasource/. - * @return bool True on success, null on failure or false if the class is already loaded - * @throws MissingDatasourceException - */ - public static function loadDataSource($connName) { - if (empty(static::$_init)) { - static::_init(); - } + if (!empty(static::$_dataSources[$name])) { + return static::$_dataSources[$name]; + } - if (is_array($connName)) { - $conn = $connName; - } else { - $conn = static::$_connectionsEnum[$connName]; - } + if (empty(static::$_connectionsEnum[$name])) { + static::_getConnectionObject($name); + } - if (class_exists($conn['classname'], false)) { - return false; - } + static::loadDataSource($name); + $conn = static::$_connectionsEnum[$name]; + $class = $conn['classname']; - $plugin = $package = null; - if (!empty($conn['plugin'])) { - $plugin = $conn['plugin'] . '.'; - } - if (!empty($conn['package'])) { - $package = '/' . $conn['package']; - } + if (strpos(App::location($class), 'Datasource') === false) { + throw new MissingDatasourceException([ + 'class' => $class, + 'plugin' => null, + 'message' => 'Datasource is not found in Model/Datasource package.' + ]); + } + static::$_dataSources[$name] = new $class(static::$config->{$name}); + static::$_dataSources[$name]->configKeyName = $name; - App::uses($conn['classname'], $plugin . 'Model/Datasource' . $package); - if (!class_exists($conn['classname'])) { - throw new MissingDatasourceException(array( - 'class' => $conn['classname'], - 'plugin' => substr($plugin, 0, -1) - )); - } - return true; - } + return static::$_dataSources[$name]; + } -/** - * Returns a list of connections - * - * @return array An associative array of elements where the key is the connection name - * (as defined in Connections), and the value is an array with keys 'filename' and 'classname'. - */ - public static function enumConnectionObjects() { - if (empty(static::$_init)) { - static::_init(); - } - return (array)static::$config; - } + /** + * Gets a list of class and file names associated with the user-defined DataSource connections + * + * @param string $name Connection name + * @return void + * @throws MissingDatasourceConfigException + */ + protected static function _getConnectionObject($name) + { + if (!empty(static::$config->{$name})) { + static::$_connectionsEnum[$name] = static::_connectionData(static::$config->{$name}); + } else { + throw new MissingDatasourceConfigException(['config' => $name]); + } + } -/** - * Dynamically creates a DataSource object at runtime, with the given name and settings - * - * @param string $name The DataSource name - * @param array $config The DataSource configuration settings - * @return DataSource|null A reference to the DataSource object, or null if creation failed - */ - public static function create($name = '', $config = array()) { - if (empty(static::$_init)) { - static::_init(); - } + /** + * Loads the DataSource class for the given connection name + * + * @param string|array $connName A string name of the connection, as defined in app/Config/database.php, + * or an array containing the filename (without extension) and class name of the object, + * to be found in app/Model/Datasource/ or lib/Cake/Model/Datasource/. + * @return bool True on success, null on failure or false if the class is already loaded + * @throws MissingDatasourceException + */ + public static function loadDataSource($connName) + { + if (empty(static::$_init)) { + static::_init(); + } - if (empty($name) || empty($config) || array_key_exists($name, static::$_connectionsEnum)) { - return null; - } - static::$config->{$name} = $config; - static::$_connectionsEnum[$name] = static::_connectionData($config); - $return = static::getDataSource($name); - return $return; - } + if (is_array($connName)) { + $conn = $connName; + } else { + $conn = static::$_connectionsEnum[$connName]; + } -/** - * Removes a connection configuration at runtime given its name - * - * @param string $name the connection name as it was created - * @return bool success if connection was removed, false if it does not exist - */ - public static function drop($name) { - if (empty(static::$_init)) { - static::_init(); - } + if (class_exists($conn['classname'], false)) { + return false; + } - if (!isset(static::$config->{$name})) { - return false; - } - unset(static::$_connectionsEnum[$name], static::$_dataSources[$name], static::$config->{$name}); - return true; - } + $plugin = $package = null; + if (!empty($conn['plugin'])) { + $plugin = $conn['plugin'] . '.'; + } + if (!empty($conn['package'])) { + $package = '/' . $conn['package']; + } -/** - * Gets a list of class and file names associated with the user-defined DataSource connections - * - * @param string $name Connection name - * @return void - * @throws MissingDatasourceConfigException - */ - protected static function _getConnectionObject($name) { - if (!empty(static::$config->{$name})) { - static::$_connectionsEnum[$name] = static::_connectionData(static::$config->{$name}); - } else { - throw new MissingDatasourceConfigException(array('config' => $name)); - } - } + App::uses($conn['classname'], $plugin . 'Model/Datasource' . $package); + if (!class_exists($conn['classname'])) { + throw new MissingDatasourceException([ + 'class' => $conn['classname'], + 'plugin' => substr($plugin, 0, -1) + ]); + } + return true; + } -/** - * Returns the file, class name, and parent for the given driver. - * - * @param array $config Array with connection configuration. Key 'datasource' is required - * @return array An indexed array with: filename, classname, plugin and parent - */ - protected static function _connectionData($config) { - $package = $classname = $plugin = null; + /** + * Removes a connection configuration at runtime given its name + * + * @param string $name the connection name as it was created + * @return bool success if connection was removed, false if it does not exist + */ + public static function drop($name) + { + if (empty(static::$_init)) { + static::_init(); + } - list($plugin, $classname) = pluginSplit($config['datasource']); - if (strpos($classname, '/') !== false) { - $package = dirname($classname); - $classname = basename($classname); - } - return compact('package', 'classname', 'plugin'); - } + if (!isset(static::$config->{$name})) { + return false; + } + unset(static::$_connectionsEnum[$name], static::$_dataSources[$name], static::$config->{$name}); + return true; + } } diff --git a/lib/Cake/Model/Datasource/CakeSession.php b/lib/Cake/Model/Datasource/CakeSession.php index e6773142..4d2e0874 100755 --- a/lib/Cake/Model/Datasource/CakeSession.php +++ b/lib/Cake/Model/Datasource/CakeSession.php @@ -32,798 +32,818 @@ * * @package Cake.Model.Datasource */ -class CakeSession { - -/** - * True if the Session is still valid - * - * @var bool - */ - public static $valid = false; - -/** - * Error messages for this session - * - * @var array - */ - public static $error = false; - -/** - * User agent string - * - * @var string - */ - protected static $_userAgent = ''; - -/** - * Path to where the session is active. - * - * @var string - */ - public static $path = '/'; - -/** - * Error number of last occurred error - * - * @var int - */ - public static $lastError = null; - -/** - * Start time for this session. - * - * @var int - */ - public static $time = false; - -/** - * Cookie lifetime - * - * @var int - */ - public static $cookieLifeTime; - -/** - * Time when this session becomes invalid. - * - * @var int - */ - public static $sessionTime = false; - -/** - * Current Session id - * - * @var string - */ - public static $id = null; - -/** - * Hostname - * - * @var string - */ - public static $host = null; - -/** - * Session timeout multiplier factor - * - * @var int - */ - public static $timeout = null; - -/** - * Number of requests that can occur during a session time without the session being renewed. - * This feature is only used when config value `Session.autoRegenerate` is set to true. - * - * @var int - * @see CakeSession::_checkValid() - */ - public static $requestCountdown = 10; - -/** - * Whether or not the init function in this class was already called - * - * @var bool - */ - protected static $_initialized = false; - -/** - * Session cookie name - * - * @var string - */ - protected static $_cookieName = null; - -/** - * Whether or not to make `_validAgentAndTime` 3.x compatible. - * - * @var bool - */ - protected static $_useForwardsCompatibleTimeout = false; - -/** - * Whether this session is running under a CLI environment - * - * @var bool - */ - protected static $_isCLI = false; - -/** - * Pseudo constructor. - * - * @param string|null $base The base path for the Session - * @return void - */ - public static function init($base = null) { - static::$time = time(); - - if (env('HTTP_USER_AGENT') && !static::$_userAgent) { - static::$_userAgent = md5(env('HTTP_USER_AGENT') . Configure::read('Security.salt')); - } - - static::_setPath($base); - static::_setHost(env('HTTP_HOST')); - - if (!static::$_initialized) { - register_shutdown_function('session_write_close'); - } - - static::$_initialized = true; - static::$_isCLI = (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg'); - } - -/** - * Setup the Path variable - * - * @param string|null $base base path - * @return void - */ - protected static function _setPath($base = null) { - if (empty($base)) { - static::$path = '/'; - return; - } - if (strpos($base, 'index.php') !== false) { - $base = str_replace('index.php', '', $base); - } - if (strpos($base, '?') !== false) { - $base = str_replace('?', '', $base); - } - static::$path = $base; - } - -/** - * Set the host name - * - * @param string $host Hostname - * @return void - */ - protected static function _setHost($host) { - static::$host = $host; - if (strpos(static::$host, ':') !== false) { - static::$host = substr(static::$host, 0, strpos(static::$host, ':')); - } - } - -/** - * Starts the Session. - * - * @return bool True if session was started - */ - public static function start() { - if (static::started()) { - return true; - } - - $id = static::id(); - static::_startSession(); - if (!$id && static::started()) { - static::_checkValid(); - } - - static::$error = false; - static::$valid = true; - return static::started(); - } - -/** - * Determine if Session has been started. - * - * @return bool True if session has been started. - */ - public static function started() { - if (function_exists('session_status')) { - return isset($_SESSION) && (session_status() === PHP_SESSION_ACTIVE); - } - return isset($_SESSION) && session_id(); - } - -/** - * Returns true if given variable is set in session. - * - * @param string $name Variable name to check for - * @return bool True if variable is there - */ - public static function check($name) { - if (!static::_hasSession() || !static::start()) { - return false; - } - if (isset($_SESSION[$name])) { - return true; - } - - return Hash::get($_SESSION, $name) !== null; - } - -/** - * Returns the session id. - * Calling this method will not auto start the session. You might have to manually - * assert a started session. - * - * Passing an id into it, you can also replace the session id if the session - * has not already been started. - * Note that depending on the session handler, not all characters are allowed - * within the session id. For example, the file session handler only allows - * characters in the range a-z A-Z 0-9 , (comma) and - (minus). - * - * @param string|null $id Id to replace the current session id - * @return string Session id - */ - public static function id($id = null) { - if ($id) { - static::$id = $id; - session_id(static::$id); - } - if (static::started()) { - return session_id(); - } - return static::$id; - } - -/** - * Removes a variable from session. - * - * @param string $name Session variable to remove - * @return bool Success - */ - public static function delete($name) { - if (static::check($name)) { - static::_overwrite($_SESSION, Hash::remove($_SESSION, $name)); - return !static::check($name); - } - return false; - } - -/** - * Used to write new data to _SESSION, since PHP doesn't like us setting the _SESSION var itself. - * - * @param array &$old Set of old variables => values - * @param array $new New set of variable => value - * @return void - */ - protected static function _overwrite(&$old, $new) { - if (!empty($old)) { - foreach ($old as $key => $var) { - if (!isset($new[$key])) { - unset($old[$key]); - } - } - } - foreach ($new as $key => $var) { - $old[$key] = $var; - } - } - -/** - * Return error description for given error number. - * - * @param int $errorNumber Error to set - * @return string Error as string - */ - protected static function _error($errorNumber) { - if (!is_array(static::$error) || !array_key_exists($errorNumber, static::$error)) { - return false; - } - return static::$error[$errorNumber]; - } - -/** - * Returns last occurred error as a string, if any. - * - * @return mixed Error description as a string, or false. - */ - public static function error() { - if (static::$lastError) { - return static::_error(static::$lastError); - } - return false; - } - -/** - * Returns true if session is valid. - * - * @return bool Success - */ - public static function valid() { - if (static::start() && static::read('Config')) { - if (static::_validAgentAndTime() && static::$error === false) { - static::$valid = true; - } else { - static::$valid = false; - static::_setError(1, 'Session Highjacking Attempted !!!'); - } - } - return static::$valid; - } - -/** - * Tests that the user agent is valid and that the session hasn't 'timed out'. - * Since timeouts are implemented in CakeSession it checks the current static::$time - * against the time the session is set to expire. The User agent is only checked - * if Session.checkAgent == true. - * - * @return bool - */ - protected static function _validAgentAndTime() { - $userAgent = static::read('Config.userAgent'); - $time = static::read('Config.time'); - if (static::$_useForwardsCompatibleTimeout) { - $time += (Configure::read('Session.timeout') * 60); - } - $validAgent = ( - Configure::read('Session.checkAgent') === false || - isset($userAgent) && static::$_userAgent === $userAgent - ); - return ($validAgent && static::$time <= $time); - } - -/** - * Get / Set the user agent - * - * @param string|null $userAgent Set the user agent - * @return string Current user agent. - */ - public static function userAgent($userAgent = null) { - if ($userAgent) { - static::$_userAgent = $userAgent; - } - if (empty(static::$_userAgent)) { - CakeSession::init(static::$path); - } - return static::$_userAgent; - } - -/** - * Returns given session variable, or all of them, if no parameters given. - * - * @param string|null $name The name of the session variable (or a path as sent to Set.extract) - * @return mixed The value of the session variable, null if session not available, - * session not started, or provided name not found in the session, false on failure. - */ - public static function read($name = null) { - if (!static::_hasSession() || !static::start()) { - return null; - } - if ($name === null) { - return static::_returnSessionVars(); - } - $result = Hash::get($_SESSION, $name); - - if (isset($result)) { - return $result; - } - return null; - } - -/** - * Returns all session variables. - * - * @return mixed Full $_SESSION array, or false on error. - */ - protected static function _returnSessionVars() { - if (!empty($_SESSION)) { - return $_SESSION; - } - static::_setError(2, 'No Session vars set'); - return false; - } - -/** - * Writes value to given session variable name. - * - * @param string|array $name Name of variable - * @param mixed $value Value to write - * @return bool True if the write was successful, false if the write failed - */ - public static function write($name, $value = null) { - if (!static::start()) { - return false; - } - - $write = $name; - if (!is_array($name)) { - $write = array($name => $value); - } - foreach ($write as $key => $val) { - static::_overwrite($_SESSION, Hash::insert($_SESSION, $key, $val)); - if (Hash::get($_SESSION, $key) !== $val) { - return false; - } - } - return true; - } - -/** - * Reads and deletes a variable from session. - * - * @param string $name The key to read and remove (or a path as sent to Hash.extract). - * @return mixed The value of the session variable, null if session not available, - * session not started, or provided name not found in the session. - */ - public static function consume($name) { - if (empty($name)) { - return null; - } - $value = static::read($name); - if ($value !== null) { - static::_overwrite($_SESSION, Hash::remove($_SESSION, $name)); - } - return $value; - } - -/** - * Helper method to destroy invalid sessions. - * - * @return void - */ - public static function destroy() { - if (!static::started()) { - static::_startSession(); - } - - if (static::started()) { - if (session_id() && static::_hasSession()) { - session_write_close(); - session_start(); - } - session_destroy(); - unset($_COOKIE[static::_cookieName()]); - } - - $_SESSION = null; - static::$id = null; - static::$_cookieName = null; - } - -/** - * Clears the session. - * - * Optionally also clears the session id and renews the session. - * - * @param bool $renew If the session should also be renewed. Defaults to true. - * @return void - */ - public static function clear($renew = true) { - if (!$renew) { - $_SESSION = array(); - return; - } - - $_SESSION = null; - static::$id = null; - static::renew(); - } - -/** - * Helper method to initialize a session, based on CakePHP core settings. - * - * Sessions can be configured with a few shortcut names as well as have any number of ini settings declared. - * - * @return void - * @throws CakeSessionException Throws exceptions when ini_set() fails. - */ - protected static function _configureSession() { - $sessionConfig = Configure::read('Session'); - - if (isset($sessionConfig['defaults'])) { - $defaults = static::_defaultConfig($sessionConfig['defaults']); - if ($defaults) { - $sessionConfig = Hash::merge($defaults, $sessionConfig); - } - } - if (!isset($sessionConfig['ini']['session.cookie_secure']) && env('HTTPS')) { - $sessionConfig['ini']['session.cookie_secure'] = 1; - } - if (isset($sessionConfig['timeout']) && !isset($sessionConfig['cookieTimeout'])) { - $sessionConfig['cookieTimeout'] = $sessionConfig['timeout']; - } - if (isset($sessionConfig['useForwardsCompatibleTimeout']) && $sessionConfig['useForwardsCompatibleTimeout']) { - static::$_useForwardsCompatibleTimeout = true; - } - - if (!isset($sessionConfig['ini']['session.cookie_lifetime'])) { - $sessionConfig['ini']['session.cookie_lifetime'] = $sessionConfig['cookieTimeout'] * 60; - } - - if (!isset($sessionConfig['ini']['session.name'])) { - $sessionConfig['ini']['session.name'] = $sessionConfig['cookie']; - } - static::$_cookieName = $sessionConfig['ini']['session.name']; - - if (!empty($sessionConfig['handler'])) { - $sessionConfig['ini']['session.save_handler'] = 'user'; - - // In PHP7.2.0+ session.save_handler can't be set to 'user' by the user. - // https://github.com/php/php-src/commit/a93a51c3bf4ea1638ce0adc4a899cb93531b9f0d - if (version_compare(PHP_VERSION, '7.2.0', '>=')) { - unset($sessionConfig['ini']['session.save_handler']); - } - } elseif (!empty($sessionConfig['session.save_path']) && Configure::read('debug')) { - if (!is_dir($sessionConfig['session.save_path'])) { - mkdir($sessionConfig['session.save_path'], 0775, true); - } - } - - if (!isset($sessionConfig['ini']['session.gc_maxlifetime'])) { - $sessionConfig['ini']['session.gc_maxlifetime'] = $sessionConfig['timeout'] * 60; - } - if (!isset($sessionConfig['ini']['session.cookie_httponly'])) { - $sessionConfig['ini']['session.cookie_httponly'] = 1; - } - // For IE<=8 - if (!isset($sessionConfig['cacheLimiter'])) { - $sessionConfig['cacheLimiter'] = 'must-revalidate'; - } - - if (empty($_SESSION) && !headers_sent() && (!function_exists('session_status') || session_status() !== PHP_SESSION_ACTIVE)) { - if (!empty($sessionConfig['ini']) && is_array($sessionConfig['ini'])) { - foreach ($sessionConfig['ini'] as $setting => $value) { - if (ini_set($setting, $value) === false) { - throw new CakeSessionException(__d('cake_dev', 'Unable to configure the session, setting %s failed.', $setting)); - } - } - } - } - if (!empty($sessionConfig['handler']) && !isset($sessionConfig['handler']['engine'])) { - call_user_func_array('session_set_save_handler', $sessionConfig['handler']); - } - if (!empty($sessionConfig['handler']['engine']) && !headers_sent()) { - $handler = static::_getHandler($sessionConfig['handler']['engine']); - if (!function_exists('session_status') || session_status() !== PHP_SESSION_ACTIVE) { - session_set_save_handler( - array($handler, 'open'), - array($handler, 'close'), - array($handler, 'read'), - array($handler, 'write'), - array($handler, 'destroy'), - array($handler, 'gc') - ); - } - } - Configure::write('Session', $sessionConfig); - static::$sessionTime = static::$time; - if (!static::$_useForwardsCompatibleTimeout) { - static::$sessionTime += ($sessionConfig['timeout'] * 60); - } - } - -/** - * Get session cookie name. - * - * @return string - */ - protected static function _cookieName() { - if (static::$_cookieName !== null) { - return static::$_cookieName; - } - - static::init(); - static::_configureSession(); - - return static::$_cookieName = session_name(); - } - -/** - * Returns whether a session exists - * - * @return bool - */ - protected static function _hasSession() { - return static::started() - || !ini_get('session.use_cookies') - || isset($_COOKIE[static::_cookieName()]) - || static::$_isCLI - || (ini_get('session.use_trans_sid') && isset($_GET[session_name()])); - } - -/** - * Find the handler class and make sure it implements the correct interface. - * - * @param string $handler Handler name. - * @return CakeSessionHandlerInterface - * @throws CakeSessionException - */ - protected static function _getHandler($handler) { - list($plugin, $class) = pluginSplit($handler, true); - App::uses($class, $plugin . 'Model/Datasource/Session'); - if (!class_exists($class)) { - throw new CakeSessionException(__d('cake_dev', 'Could not load %s to handle the session.', $class)); - } - $handler = new $class(); - if ($handler instanceof CakeSessionHandlerInterface) { - return $handler; - } - throw new CakeSessionException(__d('cake_dev', 'Chosen SessionHandler does not implement CakeSessionHandlerInterface it cannot be used with an engine key.')); - } - -/** - * Get one of the prebaked default session configurations. - * - * @param string $name Config name. - * @return bool|array - */ - protected static function _defaultConfig($name) { - $defaults = array( - 'php' => array( - 'cookie' => 'CAKEPHP', - 'timeout' => 240, - 'ini' => array( - 'session.use_trans_sid' => 0, - 'session.cookie_path' => static::$path - ) - ), - 'cake' => array( - 'cookie' => 'CAKEPHP', - 'timeout' => 240, - 'ini' => array( - 'session.use_trans_sid' => 0, - 'url_rewriter.tags' => '', - 'session.serialize_handler' => 'php', - 'session.use_cookies' => 1, - 'session.cookie_path' => static::$path, - 'session.save_path' => TMP . 'sessions', - 'session.save_handler' => 'files' - ) - ), - 'cache' => array( - 'cookie' => 'CAKEPHP', - 'timeout' => 240, - 'ini' => array( - 'session.use_trans_sid' => 0, - 'url_rewriter.tags' => '', - 'session.use_cookies' => 1, - 'session.cookie_path' => static::$path, - 'session.save_handler' => 'user', - ), - 'handler' => array( - 'engine' => 'CacheSession', - 'config' => 'default' - ) - ), - 'database' => array( - 'cookie' => 'CAKEPHP', - 'timeout' => 240, - 'ini' => array( - 'session.use_trans_sid' => 0, - 'url_rewriter.tags' => '', - 'session.use_cookies' => 1, - 'session.cookie_path' => static::$path, - 'session.save_handler' => 'user', - 'session.serialize_handler' => 'php', - ), - 'handler' => array( - 'engine' => 'DatabaseSession', - 'model' => 'Session' - ) - ) - ); - if (isset($defaults[$name])) { - return $defaults[$name]; - } - return false; - } - -/** - * Helper method to start a session - * - * @return bool Success - */ - protected static function _startSession() { - static::init(); - session_write_close(); - static::_configureSession(); - - if (headers_sent()) { - if (empty($_SESSION)) { - $_SESSION = array(); - } - } else { - $limit = Configure::read('Session.cacheLimiter'); - if (!empty($limit)) { - session_cache_limiter($limit); - } - session_start(); - } - return true; - } - -/** - * Helper method to create a new session. - * - * @return void - */ - protected static function _checkValid() { - $config = static::read('Config'); - if ($config) { - $sessionConfig = Configure::read('Session'); - - if (static::valid()) { - static::write('Config.time', static::$sessionTime); - if (isset($sessionConfig['autoRegenerate']) && $sessionConfig['autoRegenerate'] === true) { - $check = $config['countdown']; - $check -= 1; - static::write('Config.countdown', $check); - - if ($check < 1) { - static::renew(); - static::write('Config.countdown', static::$requestCountdown); - } - } - } else { - $_SESSION = array(); - static::destroy(); - static::_setError(1, 'Session Highjacking Attempted !!!'); - static::_startSession(); - static::_writeConfig(); - } - } else { - static::_writeConfig(); - } - } - -/** - * Writes configuration variables to the session - * - * @return void - */ - protected static function _writeConfig() { - static::write('Config.userAgent', static::$_userAgent); - static::write('Config.time', static::$sessionTime); - static::write('Config.countdown', static::$requestCountdown); - } - -/** - * Restarts this session. - * - * @return void - */ - public static function renew() { - if (session_id() === '') { - return; - } - if (isset($_COOKIE[static::_cookieName()])) { - setcookie(Configure::read('Session.cookie'), '', time() - 42000, static::$path); - } - if (!headers_sent()) { - session_write_close(); - session_start(); - session_regenerate_id(true); - } - } - -/** - * Helper method to set an internal error message. - * - * @param int $errorNumber Number of the error - * @param string $errorMessage Description of the error - * @return void - */ - protected static function _setError($errorNumber, $errorMessage) { - if (static::$error === false) { - static::$error = array(); - } - static::$error[$errorNumber] = $errorMessage; - static::$lastError = $errorNumber; - } +class CakeSession +{ + + /** + * True if the Session is still valid + * + * @var bool + */ + public static $valid = false; + + /** + * Error messages for this session + * + * @var array + */ + public static $error = false; + /** + * Path to where the session is active. + * + * @var string + */ + public static $path = '/'; + /** + * Error number of last occurred error + * + * @var int + */ + public static $lastError = null; + /** + * Start time for this session. + * + * @var int + */ + public static $time = false; + /** + * Cookie lifetime + * + * @var int + */ + public static $cookieLifeTime; + /** + * Time when this session becomes invalid. + * + * @var int + */ + public static $sessionTime = false; + /** + * Current Session id + * + * @var string + */ + public static $id = null; + /** + * Hostname + * + * @var string + */ + public static $host = null; + /** + * Session timeout multiplier factor + * + * @var int + */ + public static $timeout = null; + /** + * Number of requests that can occur during a session time without the session being renewed. + * This feature is only used when config value `Session.autoRegenerate` is set to true. + * + * @var int + * @see CakeSession::_checkValid() + */ + public static $requestCountdown = 10; + /** + * User agent string + * + * @var string + */ + protected static $_userAgent = ''; + /** + * Whether or not the init function in this class was already called + * + * @var bool + */ + protected static $_initialized = false; + + /** + * Session cookie name + * + * @var string + */ + protected static $_cookieName = null; + + /** + * Whether or not to make `_validAgentAndTime` 3.x compatible. + * + * @var bool + */ + protected static $_useForwardsCompatibleTimeout = false; + + /** + * Whether this session is running under a CLI environment + * + * @var bool + */ + protected static $_isCLI = false; + + /** + * Removes a variable from session. + * + * @param string $name Session variable to remove + * @return bool Success + */ + public static function delete($name) + { + if (static::check($name)) { + static::_overwrite($_SESSION, Hash::remove($_SESSION, $name)); + return !static::check($name); + } + return false; + } + + /** + * Returns true if given variable is set in session. + * + * @param string $name Variable name to check for + * @return bool True if variable is there + */ + public static function check($name) + { + if (!static::_hasSession() || !static::start()) { + return false; + } + if (isset($_SESSION[$name])) { + return true; + } + + return Hash::get($_SESSION, $name) !== null; + } + + /** + * Returns whether a session exists + * + * @return bool + */ + protected static function _hasSession() + { + return static::started() + || !ini_get('session.use_cookies') + || isset($_COOKIE[static::_cookieName()]) + || static::$_isCLI + || (ini_get('session.use_trans_sid') && isset($_GET[session_name()])); + } + + /** + * Determine if Session has been started. + * + * @return bool True if session has been started. + */ + public static function started() + { + if (function_exists('session_status')) { + return isset($_SESSION) && (session_status() === PHP_SESSION_ACTIVE); + } + return isset($_SESSION) && session_id(); + } + + /** + * Get session cookie name. + * + * @return string + */ + protected static function _cookieName() + { + if (static::$_cookieName !== null) { + return static::$_cookieName; + } + + static::init(); + static::_configureSession(); + + return static::$_cookieName = session_name(); + } + + /** + * Pseudo constructor. + * + * @param string|null $base The base path for the Session + * @return void + */ + public static function init($base = null) + { + static::$time = time(); + + if (env('HTTP_USER_AGENT') && !static::$_userAgent) { + static::$_userAgent = md5(env('HTTP_USER_AGENT') . Configure::read('Security.salt')); + } + + static::_setPath($base); + static::_setHost(env('HTTP_HOST')); + + if (!static::$_initialized) { + register_shutdown_function('session_write_close'); + } + + static::$_initialized = true; + static::$_isCLI = (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg'); + } + + /** + * Setup the Path variable + * + * @param string|null $base base path + * @return void + */ + protected static function _setPath($base = null) + { + if (empty($base)) { + static::$path = '/'; + return; + } + if (strpos($base, 'index.php') !== false) { + $base = str_replace('index.php', '', $base); + } + if (strpos($base, '?') !== false) { + $base = str_replace('?', '', $base); + } + static::$path = $base; + } + + /** + * Set the host name + * + * @param string $host Hostname + * @return void + */ + protected static function _setHost($host) + { + static::$host = $host; + if (strpos(static::$host, ':') !== false) { + static::$host = substr(static::$host, 0, strpos(static::$host, ':')); + } + } + + /** + * Helper method to initialize a session, based on CakePHP core settings. + * + * Sessions can be configured with a few shortcut names as well as have any number of ini settings declared. + * + * @return void + * @throws CakeSessionException Throws exceptions when ini_set() fails. + */ + protected static function _configureSession() + { + $sessionConfig = Configure::read('Session'); + + if (isset($sessionConfig['defaults'])) { + $defaults = static::_defaultConfig($sessionConfig['defaults']); + if ($defaults) { + $sessionConfig = Hash::merge($defaults, $sessionConfig); + } + } + if (!isset($sessionConfig['ini']['session.cookie_secure']) && env('HTTPS')) { + $sessionConfig['ini']['session.cookie_secure'] = 1; + } + if (isset($sessionConfig['timeout']) && !isset($sessionConfig['cookieTimeout'])) { + $sessionConfig['cookieTimeout'] = $sessionConfig['timeout']; + } + if (isset($sessionConfig['useForwardsCompatibleTimeout']) && $sessionConfig['useForwardsCompatibleTimeout']) { + static::$_useForwardsCompatibleTimeout = true; + } + + if (!isset($sessionConfig['ini']['session.cookie_lifetime'])) { + $sessionConfig['ini']['session.cookie_lifetime'] = $sessionConfig['cookieTimeout'] * 60; + } + + if (!isset($sessionConfig['ini']['session.name'])) { + $sessionConfig['ini']['session.name'] = $sessionConfig['cookie']; + } + static::$_cookieName = $sessionConfig['ini']['session.name']; + + if (!empty($sessionConfig['handler'])) { + $sessionConfig['ini']['session.save_handler'] = 'user'; + + // In PHP7.2.0+ session.save_handler can't be set to 'user' by the user. + // https://github.com/php/php-src/commit/a93a51c3bf4ea1638ce0adc4a899cb93531b9f0d + if (version_compare(PHP_VERSION, '7.2.0', '>=')) { + unset($sessionConfig['ini']['session.save_handler']); + } + } else if (!empty($sessionConfig['session.save_path']) && Configure::read('debug')) { + if (!is_dir($sessionConfig['session.save_path'])) { + mkdir($sessionConfig['session.save_path'], 0775, true); + } + } + + if (!isset($sessionConfig['ini']['session.gc_maxlifetime'])) { + $sessionConfig['ini']['session.gc_maxlifetime'] = $sessionConfig['timeout'] * 60; + } + if (!isset($sessionConfig['ini']['session.cookie_httponly'])) { + $sessionConfig['ini']['session.cookie_httponly'] = 1; + } + // For IE<=8 + if (!isset($sessionConfig['cacheLimiter'])) { + $sessionConfig['cacheLimiter'] = 'must-revalidate'; + } + + if (empty($_SESSION) && !headers_sent() && (!function_exists('session_status') || session_status() !== PHP_SESSION_ACTIVE)) { + if (!empty($sessionConfig['ini']) && is_array($sessionConfig['ini'])) { + foreach ($sessionConfig['ini'] as $setting => $value) { + if (ini_set($setting, $value) === false) { + throw new CakeSessionException(__d('cake_dev', 'Unable to configure the session, setting %s failed.', $setting)); + } + } + } + } + if (!empty($sessionConfig['handler']) && !isset($sessionConfig['handler']['engine'])) { + call_user_func_array('session_set_save_handler', $sessionConfig['handler']); + } + if (!empty($sessionConfig['handler']['engine']) && !headers_sent()) { + $handler = static::_getHandler($sessionConfig['handler']['engine']); + if (!function_exists('session_status') || session_status() !== PHP_SESSION_ACTIVE) { + session_set_save_handler( + [$handler, 'open'], + [$handler, 'close'], + [$handler, 'read'], + [$handler, 'write'], + [$handler, 'destroy'], + [$handler, 'gc'] + ); + } + } + Configure::write('Session', $sessionConfig); + static::$sessionTime = static::$time; + if (!static::$_useForwardsCompatibleTimeout) { + static::$sessionTime += ($sessionConfig['timeout'] * 60); + } + } + + /** + * Get one of the prebaked default session configurations. + * + * @param string $name Config name. + * @return bool|array + */ + protected static function _defaultConfig($name) + { + $defaults = [ + 'php' => [ + 'cookie' => 'CAKEPHP', + 'timeout' => 240, + 'ini' => [ + 'session.use_trans_sid' => 0, + 'session.cookie_path' => static::$path + ] + ], + 'cake' => [ + 'cookie' => 'CAKEPHP', + 'timeout' => 240, + 'ini' => [ + 'session.use_trans_sid' => 0, + 'url_rewriter.tags' => '', + 'session.serialize_handler' => 'php', + 'session.use_cookies' => 1, + 'session.cookie_path' => static::$path, + 'session.save_path' => TMP . 'sessions', + 'session.save_handler' => 'files' + ] + ], + 'cache' => [ + 'cookie' => 'CAKEPHP', + 'timeout' => 240, + 'ini' => [ + 'session.use_trans_sid' => 0, + 'url_rewriter.tags' => '', + 'session.use_cookies' => 1, + 'session.cookie_path' => static::$path, + 'session.save_handler' => 'user', + ], + 'handler' => [ + 'engine' => 'CacheSession', + 'config' => 'default' + ] + ], + 'database' => [ + 'cookie' => 'CAKEPHP', + 'timeout' => 240, + 'ini' => [ + 'session.use_trans_sid' => 0, + 'url_rewriter.tags' => '', + 'session.use_cookies' => 1, + 'session.cookie_path' => static::$path, + 'session.save_handler' => 'user', + 'session.serialize_handler' => 'php', + ], + 'handler' => [ + 'engine' => 'DatabaseSession', + 'model' => 'Session' + ] + ] + ]; + if (isset($defaults[$name])) { + return $defaults[$name]; + } + return false; + } + + /** + * Find the handler class and make sure it implements the correct interface. + * + * @param string $handler Handler name. + * @return CakeSessionHandlerInterface + * @throws CakeSessionException + */ + protected static function _getHandler($handler) + { + list($plugin, $class) = pluginSplit($handler, true); + App::uses($class, $plugin . 'Model/Datasource/Session'); + if (!class_exists($class)) { + throw new CakeSessionException(__d('cake_dev', 'Could not load %s to handle the session.', $class)); + } + $handler = new $class(); + if ($handler instanceof CakeSessionHandlerInterface) { + return $handler; + } + throw new CakeSessionException(__d('cake_dev', 'Chosen SessionHandler does not implement CakeSessionHandlerInterface it cannot be used with an engine key.')); + } + + /** + * Starts the Session. + * + * @return bool True if session was started + */ + public static function start() + { + if (static::started()) { + return true; + } + + $id = static::id(); + static::_startSession(); + if (!$id && static::started()) { + static::_checkValid(); + } + + static::$error = false; + static::$valid = true; + return static::started(); + } + + /** + * Returns the session id. + * Calling this method will not auto start the session. You might have to manually + * assert a started session. + * + * Passing an id into it, you can also replace the session id if the session + * has not already been started. + * Note that depending on the session handler, not all characters are allowed + * within the session id. For example, the file session handler only allows + * characters in the range a-z A-Z 0-9 , (comma) and - (minus). + * + * @param string|null $id Id to replace the current session id + * @return string Session id + */ + public static function id($id = null) + { + if ($id) { + static::$id = $id; + session_id(static::$id); + } + if (static::started()) { + return session_id(); + } + return static::$id; + } + + /** + * Helper method to start a session + * + * @return bool Success + */ + protected static function _startSession() + { + static::init(); + session_write_close(); + static::_configureSession(); + + if (headers_sent()) { + if (empty($_SESSION)) { + $_SESSION = []; + } + } else { + $limit = Configure::read('Session.cacheLimiter'); + if (!empty($limit)) { + session_cache_limiter($limit); + } + session_start(); + } + return true; + } + + /** + * Helper method to create a new session. + * + * @return void + */ + protected static function _checkValid() + { + $config = static::read('Config'); + if ($config) { + $sessionConfig = Configure::read('Session'); + + if (static::valid()) { + static::write('Config.time', static::$sessionTime); + if (isset($sessionConfig['autoRegenerate']) && $sessionConfig['autoRegenerate'] === true) { + $check = $config['countdown']; + $check -= 1; + static::write('Config.countdown', $check); + + if ($check < 1) { + static::renew(); + static::write('Config.countdown', static::$requestCountdown); + } + } + } else { + $_SESSION = []; + static::destroy(); + static::_setError(1, 'Session Highjacking Attempted !!!'); + static::_startSession(); + static::_writeConfig(); + } + } else { + static::_writeConfig(); + } + } + + /** + * Returns given session variable, or all of them, if no parameters given. + * + * @param string|null $name The name of the session variable (or a path as sent to Set.extract) + * @return mixed The value of the session variable, null if session not available, + * session not started, or provided name not found in the session, false on failure. + */ + public static function read($name = null) + { + if (!static::_hasSession() || !static::start()) { + return null; + } + if ($name === null) { + return static::_returnSessionVars(); + } + $result = Hash::get($_SESSION, $name); + + if (isset($result)) { + return $result; + } + return null; + } + + /** + * Returns all session variables. + * + * @return mixed Full $_SESSION array, or false on error. + */ + protected static function _returnSessionVars() + { + if (!empty($_SESSION)) { + return $_SESSION; + } + static::_setError(2, 'No Session vars set'); + return false; + } + + /** + * Helper method to set an internal error message. + * + * @param int $errorNumber Number of the error + * @param string $errorMessage Description of the error + * @return void + */ + protected static function _setError($errorNumber, $errorMessage) + { + if (static::$error === false) { + static::$error = []; + } + static::$error[$errorNumber] = $errorMessage; + static::$lastError = $errorNumber; + } + + /** + * Returns true if session is valid. + * + * @return bool Success + */ + public static function valid() + { + if (static::start() && static::read('Config')) { + if (static::_validAgentAndTime() && static::$error === false) { + static::$valid = true; + } else { + static::$valid = false; + static::_setError(1, 'Session Highjacking Attempted !!!'); + } + } + return static::$valid; + } + + /** + * Tests that the user agent is valid and that the session hasn't 'timed out'. + * Since timeouts are implemented in CakeSession it checks the current static::$time + * against the time the session is set to expire. The User agent is only checked + * if Session.checkAgent == true. + * + * @return bool + */ + protected static function _validAgentAndTime() + { + $userAgent = static::read('Config.userAgent'); + $time = static::read('Config.time'); + if (static::$_useForwardsCompatibleTimeout) { + $time += (Configure::read('Session.timeout') * 60); + } + $validAgent = ( + Configure::read('Session.checkAgent') === false || + isset($userAgent) && static::$_userAgent === $userAgent + ); + return ($validAgent && static::$time <= $time); + } + + /** + * Writes value to given session variable name. + * + * @param string|array $name Name of variable + * @param mixed $value Value to write + * @return bool True if the write was successful, false if the write failed + */ + public static function write($name, $value = null) + { + if (!static::start()) { + return false; + } + + $write = $name; + if (!is_array($name)) { + $write = [$name => $value]; + } + foreach ($write as $key => $val) { + static::_overwrite($_SESSION, Hash::insert($_SESSION, $key, $val)); + if (Hash::get($_SESSION, $key) !== $val) { + return false; + } + } + return true; + } + + /** + * Used to write new data to _SESSION, since PHP doesn't like us setting the _SESSION var itself. + * + * @param array &$old Set of old variables => values + * @param array $new New set of variable => value + * @return void + */ + protected static function _overwrite(&$old, $new) + { + if (!empty($old)) { + foreach ($old as $key => $var) { + if (!isset($new[$key])) { + unset($old[$key]); + } + } + } + foreach ($new as $key => $var) { + $old[$key] = $var; + } + } + + /** + * Restarts this session. + * + * @return void + */ + public static function renew() + { + if (session_id() === '') { + return; + } + if (isset($_COOKIE[static::_cookieName()])) { + setcookie(Configure::read('Session.cookie'), '', time() - 42000, static::$path); + } + if (!headers_sent()) { + session_write_close(); + session_start(); + session_regenerate_id(true); + } + } + + /** + * Helper method to destroy invalid sessions. + * + * @return void + */ + public static function destroy() + { + if (!static::started()) { + static::_startSession(); + } + + if (static::started()) { + if (session_id() && static::_hasSession()) { + session_write_close(); + session_start(); + } + session_destroy(); + unset($_COOKIE[static::_cookieName()]); + } + + $_SESSION = null; + static::$id = null; + static::$_cookieName = null; + } + + /** + * Writes configuration variables to the session + * + * @return void + */ + protected static function _writeConfig() + { + static::write('Config.userAgent', static::$_userAgent); + static::write('Config.time', static::$sessionTime); + static::write('Config.countdown', static::$requestCountdown); + } + + /** + * Returns last occurred error as a string, if any. + * + * @return mixed Error description as a string, or false. + */ + public static function error() + { + if (static::$lastError) { + return static::_error(static::$lastError); + } + return false; + } + + /** + * Return error description for given error number. + * + * @param int $errorNumber Error to set + * @return string Error as string + */ + protected static function _error($errorNumber) + { + if (!is_array(static::$error) || !array_key_exists($errorNumber, static::$error)) { + return false; + } + return static::$error[$errorNumber]; + } + + /** + * Get / Set the user agent + * + * @param string|null $userAgent Set the user agent + * @return string Current user agent. + */ + public static function userAgent($userAgent = null) + { + if ($userAgent) { + static::$_userAgent = $userAgent; + } + if (empty(static::$_userAgent)) { + CakeSession::init(static::$path); + } + return static::$_userAgent; + } + + /** + * Reads and deletes a variable from session. + * + * @param string $name The key to read and remove (or a path as sent to Hash.extract). + * @return mixed The value of the session variable, null if session not available, + * session not started, or provided name not found in the session. + */ + public static function consume($name) + { + if (empty($name)) { + return null; + } + $value = static::read($name); + if ($value !== null) { + static::_overwrite($_SESSION, Hash::remove($_SESSION, $name)); + } + return $value; + } + + /** + * Clears the session. + * + * Optionally also clears the session id and renews the session. + * + * @param bool $renew If the session should also be renewed. Defaults to true. + * @return void + */ + public static function clear($renew = true) + { + if (!$renew) { + $_SESSION = []; + return; + } + + $_SESSION = null; + static::$id = null; + static::renew(); + } } diff --git a/lib/Cake/Model/Datasource/DataSource.php b/lib/Cake/Model/Datasource/DataSource.php index 6d0710b9..6e845a72 100755 --- a/lib/Cake/Model/Datasource/DataSource.php +++ b/lib/Cake/Model/Datasource/DataSource.php @@ -24,414 +24,431 @@ * @link https://book.cakephp.org/2.0/en/models/datasources.html#basic-api-for-datasources * @package Cake.Model.Datasource */ -class DataSource extends CakeObject { - -/** - * Are we connected to the DataSource? - * - * @var bool - */ - public $connected = false; - -/** - * The default configuration of a specific DataSource - * - * @var array - */ - protected $_baseConfig = array(); - -/** - * Holds references to descriptions loaded by the DataSource - * - * @var array - */ - protected $_descriptions = array(); - -/** - * Holds a list of sources (tables) contained in the DataSource - * - * @var array - */ - protected $_sources = null; - -/** - * The DataSource configuration - * - * @var array - */ - public $config = array(); - -/** - * Whether or not this DataSource is in the middle of a transaction - * - * @var bool - */ - protected $_transactionStarted = false; - -/** - * Whether or not source data like available tables and schema descriptions - * should be cached - * - * @var bool - */ - public $cacheSources = true; - -/** - * Constructor. - * - * @param array $config Array of configuration information for the datasource. - */ - public function __construct($config = array()) { - parent::__construct(); - $this->setConfig($config); - } - -/** - * Caches/returns cached results for child instances - * - * @param mixed $data Unused in this class. - * @return array|null Array of sources available in this datasource. - */ - public function listSources($data = null) { - if ($this->cacheSources === false) { - return null; - } - - if ($this->_sources !== null) { - return $this->_sources; - } - - $key = ConnectionManager::getSourceName($this) . '_' . $this->config['database'] . '_list'; - $key = preg_replace('/[^A-Za-z0-9_\-.+]/', '_', $key); - $sources = Cache::read($key, '_cake_model_'); - - if (empty($sources)) { - $sources = $data; - Cache::write($key, $data, '_cake_model_'); - } - - return $this->_sources = $sources; - } - -/** - * Returns a Model description (metadata) or null if none found. - * - * @param Model|string $model The model to describe. - * @return array|null Array of Metadata for the $model - */ - public function describe($model) { - if ($this->cacheSources === false) { - return null; - } - if (is_string($model)) { - $table = $model; - } else { - $table = $model->tablePrefix . $model->table; - } - - if (isset($this->_descriptions[$table])) { - return $this->_descriptions[$table]; - } - $cache = $this->_cacheDescription($table); - - if ($cache !== null) { - $this->_descriptions[$table] =& $cache; - return $cache; - } - return null; - } - -/** - * Begin a transaction - * - * @return bool Returns true if a transaction is not in progress - */ - public function begin() { - return !$this->_transactionStarted; - } - -/** - * Commit a transaction - * - * @return bool Returns true if a transaction is in progress - */ - public function commit() { - return $this->_transactionStarted; - } - -/** - * Rollback a transaction - * - * @return bool Returns true if a transaction is in progress - */ - public function rollback() { - return $this->_transactionStarted; - } - -/** - * Converts column types to basic types - * - * @param string $real Real column type (i.e. "varchar(255)") - * @return string Abstract column type (i.e. "string") - */ - public function column($real) { - return false; - } - -/** - * Used to create new records. The "C" CRUD. - * - * To-be-overridden in subclasses. - * - * @param Model $Model The Model to be created. - * @param array $fields An Array of fields to be saved. - * @param array $values An Array of values to save. - * @return bool success - */ - public function create(Model $Model, $fields = null, $values = null) { - return false; - } - -/** - * Used to read records from the Datasource. The "R" in CRUD - * - * To-be-overridden in subclasses. - * - * @param Model $Model The model being read. - * @param array $queryData An array of query data used to find the data you want - * @param int $recursive Number of levels of association - * @return mixed - */ - public function read(Model $Model, $queryData = array(), $recursive = null) { - return false; - } - -/** - * Update a record(s) in the datasource. - * - * To-be-overridden in subclasses. - * - * @param Model $Model Instance of the model class being updated - * @param array $fields Array of fields to be updated - * @param array $values Array of values to be update $fields to. - * @param mixed $conditions The array of conditions to use. - * @return bool Success - */ - public function update(Model $Model, $fields = null, $values = null, $conditions = null) { - return false; - } - -/** - * Delete a record(s) in the datasource. - * - * To-be-overridden in subclasses. - * - * @param Model $Model The model class having record(s) deleted - * @param mixed $conditions The conditions to use for deleting. - * @return bool Success - */ - public function delete(Model $Model, $conditions = null) { - return false; - } - -/** - * Returns the ID generated from the previous INSERT operation. - * - * @param mixed $source The source name. - * @return mixed Last ID key generated in previous INSERT - */ - public function lastInsertId($source = null) { - return false; - } - -/** - * Returns the number of rows returned by last operation. - * - * @param mixed $source The source name. - * @return int Number of rows returned by last operation - */ - public function lastNumRows($source = null) { - return false; - } - -/** - * Returns the number of rows affected by last query. - * - * @param mixed $source The source name. - * @return int Number of rows affected by last query. - */ - public function lastAffected($source = null) { - return false; - } - -/** - * Check whether the conditions for the Datasource being available - * are satisfied. Often used from connect() to check for support - * before establishing a connection. - * - * @return bool Whether or not the Datasources conditions for use are met. - */ - public function enabled() { - return true; - } - -/** - * Sets the configuration for the DataSource. - * Merges the $config information with the _baseConfig and the existing $config property. - * - * @param array $config The configuration array - * @return void - */ - public function setConfig($config = array()) { - $this->config = array_merge($this->_baseConfig, $this->config, $config); - } - -/** - * Cache the DataSource description - * - * @param string $object The name of the object (model) to cache - * @param mixed $data The description of the model, usually a string or array - * @return mixed - */ - protected function _cacheDescription($object, $data = null) { - if ($this->cacheSources === false) { - return null; - } - - if ($data !== null) { - $this->_descriptions[$object] =& $data; - } - - $key = ConnectionManager::getSourceName($this) . '_' . $object; - $cache = Cache::read($key, '_cake_model_'); - - if (empty($cache)) { - $cache = $data; - Cache::write($key, $cache, '_cake_model_'); - } - - return $cache; - } - -/** - * Replaces `{$__cakeID__$}` and `{$__cakeForeignKey__$}` placeholders in query data. - * - * @param string $query Query string needing replacements done. - * @param array $data Array of data with values that will be inserted in placeholders. - * @param string $association Name of association model being replaced. - * @param Model $Model Model instance. - * @param array $stack The context stack. - * @return mixed String of query data with placeholders replaced, or false on failure. - */ - public function insertQueryData($query, $data, $association, Model $Model, $stack) { - $keys = array('{$__cakeID__$}', '{$__cakeForeignKey__$}'); - - $modelAlias = $Model->alias; - - foreach ($keys as $key) { - if (strpos($query, $key) === false) { - continue; - } - - $insertKey = $InsertModel = null; - switch ($key) { - case '{$__cakeID__$}': - $InsertModel = $Model; - $insertKey = $Model->primaryKey; - - break; - case '{$__cakeForeignKey__$}': - foreach ($Model->associations() as $type) { - foreach ($Model->{$type} as $assoc => $assocData) { - if ($assoc !== $association) { - continue; - } - - if (isset($assocData['foreignKey'])) { - $InsertModel = $Model->{$assoc}; - $insertKey = $assocData['foreignKey']; - } - - break 3; - } - } - - break; - } - - $val = $dataType = null; - if (!empty($insertKey) && !empty($InsertModel)) { - if (isset($data[$modelAlias][$insertKey])) { - $val = $data[$modelAlias][$insertKey]; - } elseif (isset($data[$association][$insertKey])) { - $val = $data[$association][$insertKey]; - } else { - $found = false; - foreach (array_reverse($stack) as $assocData) { - if (is_string($assocData) && isset($data[$assocData]) && isset($data[$assocData][$insertKey])) { - $val = $data[$assocData][$insertKey]; - $found = true; - break; - } - } - - if (!$found) { - $val = ''; - } - } - - $dataType = $InsertModel->getColumnType($InsertModel->primaryKey); - } - - if (empty($val) && $val !== '0') { - return false; - } - - $query = str_replace($key, $this->value($val, $dataType), $query); - } - - return $query; - } - -/** - * To-be-overridden in subclasses. - * - * @param Model $Model Model instance - * @param string $key Key name to make - * @return string Key name for model. - */ - public function resolveKey(Model $Model, $key) { - return $Model->alias . $key; - } - -/** - * Returns the schema name. Override this in subclasses. - * - * @return string|null The schema name - */ - public function getSchemaName() { - return null; - } - -/** - * Closes a connection. Override in subclasses. - * - * @return bool - */ - public function close() { - return $this->connected = false; - } - -/** - * Closes the current datasource. - */ - public function __destruct() { - if ($this->_transactionStarted) { - $this->rollback(); - } - if ($this->connected) { - $this->close(); - } - } +class DataSource extends CakeObject +{ + + /** + * Are we connected to the DataSource? + * + * @var bool + */ + public $connected = false; + /** + * The DataSource configuration + * + * @var array + */ + public $config = []; + /** + * Whether or not source data like available tables and schema descriptions + * should be cached + * + * @var bool + */ + public $cacheSources = true; + /** + * The default configuration of a specific DataSource + * + * @var array + */ + protected $_baseConfig = []; + /** + * Holds references to descriptions loaded by the DataSource + * + * @var array + */ + protected $_descriptions = []; + /** + * Holds a list of sources (tables) contained in the DataSource + * + * @var array + */ + protected $_sources = null; + /** + * Whether or not this DataSource is in the middle of a transaction + * + * @var bool + */ + protected $_transactionStarted = false; + + /** + * Constructor. + * + * @param array $config Array of configuration information for the datasource. + */ + public function __construct($config = []) + { + parent::__construct(); + $this->setConfig($config); + } + + /** + * Sets the configuration for the DataSource. + * Merges the $config information with the _baseConfig and the existing $config property. + * + * @param array $config The configuration array + * @return void + */ + public function setConfig($config = []) + { + $this->config = array_merge($this->_baseConfig, $this->config, $config); + } + + /** + * Caches/returns cached results for child instances + * + * @param mixed $data Unused in this class. + * @return array|null Array of sources available in this datasource. + */ + public function listSources($data = null) + { + if ($this->cacheSources === false) { + return null; + } + + if ($this->_sources !== null) { + return $this->_sources; + } + + $key = ConnectionManager::getSourceName($this) . '_' . $this->config['database'] . '_list'; + $key = preg_replace('/[^A-Za-z0-9_\-.+]/', '_', $key); + $sources = Cache::read($key, '_cake_model_'); + + if (empty($sources)) { + $sources = $data; + Cache::write($key, $data, '_cake_model_'); + } + + return $this->_sources = $sources; + } + + /** + * Returns a Model description (metadata) or null if none found. + * + * @param Model|string $model The model to describe. + * @return array|null Array of Metadata for the $model + */ + public function describe($model) + { + if ($this->cacheSources === false) { + return null; + } + if (is_string($model)) { + $table = $model; + } else { + $table = $model->tablePrefix . $model->table; + } + + if (isset($this->_descriptions[$table])) { + return $this->_descriptions[$table]; + } + $cache = $this->_cacheDescription($table); + + if ($cache !== null) { + $this->_descriptions[$table] =& $cache; + return $cache; + } + return null; + } + + /** + * Cache the DataSource description + * + * @param string $object The name of the object (model) to cache + * @param mixed $data The description of the model, usually a string or array + * @return mixed + */ + protected function _cacheDescription($object, $data = null) + { + if ($this->cacheSources === false) { + return null; + } + + if ($data !== null) { + $this->_descriptions[$object] =& $data; + } + + $key = ConnectionManager::getSourceName($this) . '_' . $object; + $cache = Cache::read($key, '_cake_model_'); + + if (empty($cache)) { + $cache = $data; + Cache::write($key, $cache, '_cake_model_'); + } + + return $cache; + } + + /** + * Begin a transaction + * + * @return bool Returns true if a transaction is not in progress + */ + public function begin() + { + return !$this->_transactionStarted; + } + + /** + * Commit a transaction + * + * @return bool Returns true if a transaction is in progress + */ + public function commit() + { + return $this->_transactionStarted; + } + + /** + * Converts column types to basic types + * + * @param string $real Real column type (i.e. "varchar(255)") + * @return string Abstract column type (i.e. "string") + */ + public function column($real) + { + return false; + } + + /** + * Used to create new records. The "C" CRUD. + * + * To-be-overridden in subclasses. + * + * @param Model $Model The Model to be created. + * @param array $fields An Array of fields to be saved. + * @param array $values An Array of values to save. + * @return bool success + */ + public function create(Model $Model, $fields = null, $values = null) + { + return false; + } + + /** + * Used to read records from the Datasource. The "R" in CRUD + * + * To-be-overridden in subclasses. + * + * @param Model $Model The model being read. + * @param array $queryData An array of query data used to find the data you want + * @param int $recursive Number of levels of association + * @return mixed + */ + public function read(Model $Model, $queryData = [], $recursive = null) + { + return false; + } + + /** + * Update a record(s) in the datasource. + * + * To-be-overridden in subclasses. + * + * @param Model $Model Instance of the model class being updated + * @param array $fields Array of fields to be updated + * @param array $values Array of values to be update $fields to. + * @param mixed $conditions The array of conditions to use. + * @return bool Success + */ + public function update(Model $Model, $fields = null, $values = null, $conditions = null) + { + return false; + } + + /** + * Delete a record(s) in the datasource. + * + * To-be-overridden in subclasses. + * + * @param Model $Model The model class having record(s) deleted + * @param mixed $conditions The conditions to use for deleting. + * @return bool Success + */ + public function delete(Model $Model, $conditions = null) + { + return false; + } + + /** + * Returns the ID generated from the previous INSERT operation. + * + * @param mixed $source The source name. + * @return mixed Last ID key generated in previous INSERT + */ + public function lastInsertId($source = null) + { + return false; + } + + /** + * Returns the number of rows returned by last operation. + * + * @param mixed $source The source name. + * @return int Number of rows returned by last operation + */ + public function lastNumRows($source = null) + { + return false; + } + + /** + * Returns the number of rows affected by last query. + * + * @param mixed $source The source name. + * @return int Number of rows affected by last query. + */ + public function lastAffected($source = null) + { + return false; + } + + /** + * Check whether the conditions for the Datasource being available + * are satisfied. Often used from connect() to check for support + * before establishing a connection. + * + * @return bool Whether or not the Datasources conditions for use are met. + */ + public function enabled() + { + return true; + } + + /** + * Replaces `{$__cakeID__$}` and `{$__cakeForeignKey__$}` placeholders in query data. + * + * @param string $query Query string needing replacements done. + * @param array $data Array of data with values that will be inserted in placeholders. + * @param string $association Name of association model being replaced. + * @param Model $Model Model instance. + * @param array $stack The context stack. + * @return mixed String of query data with placeholders replaced, or false on failure. + */ + public function insertQueryData($query, $data, $association, Model $Model, $stack) + { + $keys = ['{$__cakeID__$}', '{$__cakeForeignKey__$}']; + + $modelAlias = $Model->alias; + + foreach ($keys as $key) { + if (strpos($query, $key) === false) { + continue; + } + + $insertKey = $InsertModel = null; + switch ($key) { + case '{$__cakeID__$}': + $InsertModel = $Model; + $insertKey = $Model->primaryKey; + + break; + case '{$__cakeForeignKey__$}': + foreach ($Model->associations() as $type) { + foreach ($Model->{$type} as $assoc => $assocData) { + if ($assoc !== $association) { + continue; + } + + if (isset($assocData['foreignKey'])) { + $InsertModel = $Model->{$assoc}; + $insertKey = $assocData['foreignKey']; + } + + break 3; + } + } + + break; + } + + $val = $dataType = null; + if (!empty($insertKey) && !empty($InsertModel)) { + if (isset($data[$modelAlias][$insertKey])) { + $val = $data[$modelAlias][$insertKey]; + } else if (isset($data[$association][$insertKey])) { + $val = $data[$association][$insertKey]; + } else { + $found = false; + foreach (array_reverse($stack) as $assocData) { + if (is_string($assocData) && isset($data[$assocData]) && isset($data[$assocData][$insertKey])) { + $val = $data[$assocData][$insertKey]; + $found = true; + break; + } + } + + if (!$found) { + $val = ''; + } + } + + $dataType = $InsertModel->getColumnType($InsertModel->primaryKey); + } + + if (empty($val) && $val !== '0') { + return false; + } + + $query = str_replace($key, $this->value($val, $dataType), $query); + } + + return $query; + } + + /** + * To-be-overridden in subclasses. + * + * @param Model $Model Model instance + * @param string $key Key name to make + * @return string Key name for model. + */ + public function resolveKey(Model $Model, $key) + { + return $Model->alias . $key; + } + + /** + * Returns the schema name. Override this in subclasses. + * + * @return string|null The schema name + */ + public function getSchemaName() + { + return null; + } + + /** + * Closes the current datasource. + */ + public function __destruct() + { + if ($this->_transactionStarted) { + $this->rollback(); + } + if ($this->connected) { + $this->close(); + } + } + + /** + * Rollback a transaction + * + * @return bool Returns true if a transaction is in progress + */ + public function rollback() + { + return $this->_transactionStarted; + } + + /** + * Closes a connection. Override in subclasses. + * + * @return bool + */ + public function close() + { + return $this->connected = false; + } } diff --git a/lib/Cake/Model/Datasource/Database/Mysql.php b/lib/Cake/Model/Datasource/Database/Mysql.php index 682c742f..98d250fb 100755 --- a/lib/Cake/Model/Datasource/Database/Mysql.php +++ b/lib/Cake/Model/Datasource/Database/Mysql.php @@ -25,897 +25,915 @@ * * @package Cake.Model.Datasource.Database */ -class Mysql extends DboSource { - -/** - * Datasource description - * - * @var string - */ - public $description = "MySQL DBO Driver"; - -/** - * Base configuration settings for MySQL driver - * - * @var array - */ - protected $_baseConfig = array( - 'persistent' => true, - 'host' => 'localhost', - 'login' => 'root', - 'password' => '', - 'database' => 'cake', - 'port' => '3306', - 'flags' => array() - ); - -/** - * Reference to the PDO object connection - * - * @var PDO - */ - protected $_connection = null; - -/** - * Start quote - * - * @var string - */ - public $startQuote = "`"; - -/** - * End quote - * - * @var string - */ - public $endQuote = "`"; - -/** - * use alias for update and delete. Set to true if version >= 4.1 - * - * @var bool - */ - protected $_useAlias = true; - -/** - * List of engine specific additional field parameters used on table creating - * - * @var array - */ - public $fieldParameters = array( - 'charset' => array('value' => 'CHARACTER SET', 'quote' => false, 'join' => ' ', 'column' => false, 'position' => 'beforeDefault'), - 'collate' => array('value' => 'COLLATE', 'quote' => false, 'join' => ' ', 'column' => 'Collation', 'position' => 'beforeDefault'), - 'comment' => array('value' => 'COMMENT', 'quote' => true, 'join' => ' ', 'column' => 'Comment', 'position' => 'afterDefault'), - 'unsigned' => array( - 'value' => 'UNSIGNED', - 'quote' => false, - 'join' => ' ', - 'column' => false, - 'position' => 'beforeDefault', - 'noVal' => true, - 'options' => array(true), - 'types' => array('integer', 'smallinteger', 'tinyinteger', 'float', 'decimal', 'biginteger') - ) - ); - -/** - * List of table engine specific parameters used on table creating - * - * @var array - */ - public $tableParameters = array( - 'charset' => array('value' => 'DEFAULT CHARSET', 'quote' => false, 'join' => '=', 'column' => 'charset'), - 'collate' => array('value' => 'COLLATE', 'quote' => false, 'join' => '=', 'column' => 'Collation'), - 'engine' => array('value' => 'ENGINE', 'quote' => false, 'join' => '=', 'column' => 'Engine'), - 'comment' => array('value' => 'COMMENT', 'quote' => true, 'join' => '=', 'column' => 'Comment'), - ); - -/** - * MySQL column definition - * - * @var array - * @link https://dev.mysql.com/doc/refman/5.7/en/data-types.html MySQL Data Types - */ - public $columns = array( - 'primary_key' => array('name' => 'NOT NULL AUTO_INCREMENT'), - 'string' => array('name' => 'varchar', 'limit' => '255'), - 'text' => array('name' => 'text'), - 'enum' => array('name' => 'enum'), - 'biginteger' => array('name' => 'bigint', 'limit' => '20'), - 'integer' => array('name' => 'int', 'limit' => '11', 'formatter' => 'intval'), - 'smallinteger' => array('name' => 'smallint', 'limit' => '6', 'formatter' => 'intval'), - 'tinyinteger' => array('name' => 'tinyint', 'limit' => '4', 'formatter' => 'intval'), - 'float' => array('name' => 'float', 'formatter' => 'floatval'), - 'decimal' => array('name' => 'decimal', 'formatter' => 'floatval'), - 'datetime' => array('name' => 'datetime', 'format' => 'Y-m-d H:i:s', 'formatter' => 'date'), - 'timestamp' => array('name' => 'timestamp', 'format' => 'Y-m-d H:i:s', 'formatter' => 'date'), - 'time' => array('name' => 'time', 'format' => 'H:i:s', 'formatter' => 'date'), - 'date' => array('name' => 'date', 'format' => 'Y-m-d', 'formatter' => 'date'), - 'binary' => array('name' => 'blob'), - 'boolean' => array('name' => 'tinyint', 'limit' => '1') - ); - -/** - * Mapping of collation names to character set names - * - * @var array - */ - protected $_charsets = array(); - -/** - * Connects to the database using options in the given configuration array. - * - * MySQL supports a few additional options that other drivers do not: - * - * - `unix_socket` Set to the path of the MySQL sock file. Can be used in place - * of host + port. - * - `ssl_key` SSL key file for connecting via SSL. Must be combined with `ssl_cert`. - * - `ssl_cert` The SSL certificate to use when connecting via SSL. Must be - * combined with `ssl_key`. - * - `ssl_ca` The certificate authority for SSL connections. - * - * @return bool True if the database could be connected, else false - * @throws MissingConnectionException - */ - public function connect() { - $config = $this->config; - $this->connected = false; - - $flags = $config['flags'] + array( - PDO::ATTR_PERSISTENT => $config['persistent'], - PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => true, - PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION - ); - - if (!empty($config['encoding'])) { - $flags[PDO::MYSQL_ATTR_INIT_COMMAND] = 'SET NAMES ' . $config['encoding']; - } - if (!empty($config['ssl_key']) && !empty($config['ssl_cert'])) { - $flags[PDO::MYSQL_ATTR_SSL_KEY] = $config['ssl_key']; - $flags[PDO::MYSQL_ATTR_SSL_CERT] = $config['ssl_cert']; - } - if (!empty($config['ssl_ca'])) { - $flags[PDO::MYSQL_ATTR_SSL_CA] = $config['ssl_ca']; - } - if (empty($config['unix_socket'])) { - $dsn = "mysql:host={$config['host']};port={$config['port']};dbname={$config['database']}"; - } else { - $dsn = "mysql:unix_socket={$config['unix_socket']};dbname={$config['database']}"; - } - - try { - $this->_connection = new PDO( - $dsn, - $config['login'], - $config['password'], - $flags - ); - $this->connected = true; - if (!empty($config['settings'])) { - foreach ($config['settings'] as $key => $value) { - $this->_execute("SET $key=$value"); - } - } - } catch (PDOException $e) { - throw new MissingConnectionException(array( - 'class' => get_class($this), - 'message' => $e->getMessage() - )); - } - - $this->_charsets = array(); - $this->_useAlias = (bool)version_compare($this->getVersion(), "4.1", ">="); - - return $this->connected; - } - -/** - * Check whether the MySQL extension is installed/loaded - * - * @return bool - */ - public function enabled() { - return in_array('mysql', PDO::getAvailableDrivers()); - } - -/** - * Returns an array of sources (tables) in the database. - * - * @param mixed $data List of tables. - * @return array Array of table names in the database - */ - public function listSources($data = null) { - $cache = parent::listSources(); - if ($cache) { - return $cache; - } - $result = $this->_execute('SHOW TABLES FROM ' . $this->name($this->config['database'])); - - if (!$result) { - $result->closeCursor(); - return array(); - } - $tables = array(); - - while ($line = $result->fetch(PDO::FETCH_NUM)) { - $tables[] = $line[0]; - } - - $result->closeCursor(); - parent::listSources($tables); - return $tables; - } - -/** - * Builds a map of the columns contained in a result - * - * @param PDOStatement $results The results to format. - * @return void - */ - public function resultSet($results) { - $this->map = array(); - $numFields = $results->columnCount(); - $index = 0; - - while ($numFields-- > 0) { - $column = $results->getColumnMeta($index); - if ($column['len'] === 1 && (empty($column['native_type']) || $column['native_type'] === 'TINY')) { - $type = 'boolean'; - } else { - $type = empty($column['native_type']) ? 'string' : $column['native_type']; - } - if (!empty($column['table']) && strpos($column['name'], $this->virtualFieldSeparator) === false) { - $this->map[$index++] = array($column['table'], $column['name'], $type); - } else { - $this->map[$index++] = array(0, $column['name'], $type); - } - } - } - -/** - * Fetches the next row from the current result set - * - * @return mixed array with results fetched and mapped to column names or false if there is no results left to fetch - */ - public function fetchResult() { - if ($row = $this->_result->fetch(PDO::FETCH_NUM)) { - $resultRow = array(); - foreach ($this->map as $col => $meta) { - list($table, $column, $type) = $meta; - $resultRow[$table][$column] = $row[$col]; - if ($type === 'boolean' && $row[$col] !== null) { - $resultRow[$table][$column] = $this->boolean($resultRow[$table][$column]); - } - } - return $resultRow; - } - $this->_result->closeCursor(); - return false; - } - -/** - * Gets the database encoding - * - * @return string The database encoding - */ - public function getEncoding() { - return $this->_execute('SHOW VARIABLES LIKE ?', array('character_set_client'))->fetchObject()->Value; - } - -/** - * Query charset by collation - * - * @param string $name Collation name - * @return string|false Character set name - */ - public function getCharsetName($name) { - if ((bool)version_compare($this->getVersion(), "5", "<")) { - return false; - } - if (isset($this->_charsets[$name])) { - return $this->_charsets[$name]; - } - $r = $this->_execute( - 'SELECT CHARACTER_SET_NAME FROM INFORMATION_SCHEMA.COLLATIONS WHERE COLLATION_NAME = ?', - array($name) - ); - $cols = $r->fetch(PDO::FETCH_ASSOC); - - if (isset($cols['CHARACTER_SET_NAME'])) { - $this->_charsets[$name] = $cols['CHARACTER_SET_NAME']; - } else { - $this->_charsets[$name] = false; - } - return $this->_charsets[$name]; - } - -/** - * Returns an array of the fields in given table name. - * - * @param Model|string $model Name of database table to inspect or model instance - * @return array Fields in table. Keys are name and type - * @throws CakeException - */ - public function describe($model) { - $key = $this->fullTableName($model, false); - $cache = parent::describe($key); - if ($cache) { - return $cache; - } - $table = $this->fullTableName($model); - - $fields = false; - $cols = $this->_execute('SHOW FULL COLUMNS FROM ' . $table); - if (!$cols) { - throw new CakeException(__d('cake_dev', 'Could not describe table for %s', $table)); - } - - while ($column = $cols->fetch(PDO::FETCH_OBJ)) { - $fields[$column->Field] = array( - 'type' => $this->column($column->Type), - 'null' => ($column->Null === 'YES' ? true : false), - 'default' => $column->Default, - 'length' => $this->length($column->Type) - ); - if (in_array($fields[$column->Field]['type'], $this->fieldParameters['unsigned']['types'], true)) { - $fields[$column->Field]['unsigned'] = $this->_unsigned($column->Type); - } - if (in_array($fields[$column->Field]['type'], array('timestamp', 'datetime')) && - in_array(strtoupper($column->Default), array('CURRENT_TIMESTAMP', 'CURRENT_TIMESTAMP()')) - ) { - $fields[$column->Field]['default'] = null; - } - if (!empty($column->Key) && isset($this->index[$column->Key])) { - $fields[$column->Field]['key'] = $this->index[$column->Key]; - } - foreach ($this->fieldParameters as $name => $value) { - if (!empty($column->{$value['column']})) { - $fields[$column->Field][$name] = $column->{$value['column']}; - } - } - if (isset($fields[$column->Field]['collate'])) { - $charset = $this->getCharsetName($fields[$column->Field]['collate']); - if ($charset) { - $fields[$column->Field]['charset'] = $charset; - } - } - } - $this->_cacheDescription($key, $fields); - $cols->closeCursor(); - return $fields; - } - -/** - * Generates and executes an SQL UPDATE statement for given model, fields, and values. - * - * @param Model $model The model to update. - * @param array $fields The fields to update. - * @param array $values The values to set. - * @param mixed $conditions The conditions to use. - * @return bool - */ - public function update(Model $model, $fields = array(), $values = null, $conditions = null) { - if (!$this->_useAlias) { - return parent::update($model, $fields, $values, $conditions); - } - - if (!$values) { - $combined = $fields; - } else { - $combined = array_combine($fields, $values); - } - - $alias = $joins = false; - $fields = $this->_prepareUpdateFields($model, $combined, empty($conditions), !empty($conditions)); - $fields = implode(', ', $fields); - $table = $this->fullTableName($model); - - if (!empty($conditions)) { - $alias = $this->name($model->alias); - if ($model->name === $model->alias) { - $joins = implode(' ', $this->_getJoins($model)); - } - } - $conditions = $this->conditions($this->defaultConditions($model, $conditions, $alias), true, true, $model); - - if ($conditions === false) { - return false; - } - - if (!$this->execute($this->renderStatement('update', compact('table', 'alias', 'joins', 'fields', 'conditions')))) { - $model->onError(); - return false; - } - return true; - } - -/** - * Generates and executes an SQL DELETE statement for given id/conditions on given model. - * - * @param Model $model The model to delete from. - * @param mixed $conditions The conditions to use. - * @return bool Success - */ - public function delete(Model $model, $conditions = null) { - if (!$this->_useAlias) { - return parent::delete($model, $conditions); - } - $alias = $this->name($model->alias); - $table = $this->fullTableName($model); - $joins = implode(' ', $this->_getJoins($model)); - - if (empty($conditions)) { - $alias = $joins = false; - } - $complexConditions = $this->_deleteNeedsComplexConditions($model, $conditions); - if (!$complexConditions) { - $joins = false; - } - - $conditions = $this->conditions($this->defaultConditions($model, $conditions, $alias), true, true, $model); - if ($conditions === false) { - return false; - } - if ($this->execute($this->renderStatement('delete', compact('alias', 'table', 'joins', 'conditions'))) === false) { - $model->onError(); - return false; - } - return true; - } - -/** - * Checks whether complex conditions are needed for a delete with the given conditions. - * - * @param Model $model The model to delete from. - * @param mixed $conditions The conditions to use. - * @return bool Whether or not complex conditions are needed - */ - protected function _deleteNeedsComplexConditions(Model $model, $conditions) { - $fields = array_keys($this->describe($model)); - foreach ((array)$conditions as $key => $value) { - if (in_array(strtolower(trim($key)), $this->_sqlBoolOps, true)) { - if ($this->_deleteNeedsComplexConditions($model, $value)) { - return true; - } - } elseif (strpos($key, $model->alias) === false && !in_array($key, $fields, true)) { - return true; - } - } - return false; - } - -/** - * Sets the database encoding - * - * @param string $enc Database encoding - * @return bool - */ - public function setEncoding($enc) { - return $this->_execute('SET NAMES ' . $enc) !== false; - } - -/** - * Returns an array of the indexes in given datasource name. - * - * @param string $model Name of model to inspect - * @return array Fields in table. Keys are column and unique - */ - public function index($model) { - $index = array(); - $table = $this->fullTableName($model); - $old = version_compare($this->getVersion(), '4.1', '<='); - if ($table) { - $indexes = $this->_execute('SHOW INDEX FROM ' . $table); - // @codingStandardsIgnoreStart - // MySQL columns don't match the cakephp conventions. - while ($idx = $indexes->fetch(PDO::FETCH_OBJ)) { - if ($old) { - $idx = (object)current((array)$idx); - } - if (!isset($index[$idx->Key_name]['column'])) { - $col = array(); - $index[$idx->Key_name]['column'] = $idx->Column_name; - - if ($idx->Index_type === 'FULLTEXT') { - $index[$idx->Key_name]['type'] = strtolower($idx->Index_type); - } else { - $index[$idx->Key_name]['unique'] = (int)($idx->Non_unique == 0); - } - } else { - if (!empty($index[$idx->Key_name]['column']) && !is_array($index[$idx->Key_name]['column'])) { - $col[] = $index[$idx->Key_name]['column']; - } - $col[] = $idx->Column_name; - $index[$idx->Key_name]['column'] = $col; - } - if (!empty($idx->Sub_part)) { - if (!isset($index[$idx->Key_name]['length'])) { - $index[$idx->Key_name]['length'] = array(); - } - $index[$idx->Key_name]['length'][$idx->Column_name] = $idx->Sub_part; - } - } - // @codingStandardsIgnoreEnd - $indexes->closeCursor(); - } - return $index; - } - -/** - * Generate a MySQL Alter Table syntax for the given Schema comparison - * - * @param array $compare Result of a CakeSchema::compare() - * @param string $table The table name. - * @return string|false String of alter statements to make. - */ - public function alterSchema($compare, $table = null) { - if (!is_array($compare)) { - return false; - } - $out = ''; - $colList = array(); - foreach ($compare as $curTable => $types) { - $indexes = $tableParameters = $colList = array(); - if (!$table || $table === $curTable) { - $out .= 'ALTER TABLE ' . $this->fullTableName($curTable) . " \n"; - foreach ($types as $type => $column) { - if (isset($column['indexes'])) { - $indexes[$type] = $column['indexes']; - unset($column['indexes']); - } - if (isset($column['tableParameters'])) { - $tableParameters[$type] = $column['tableParameters']; - unset($column['tableParameters']); - } - switch ($type) { - case 'add': - foreach ($column as $field => $col) { - $col['name'] = $field; - $alter = 'ADD ' . $this->buildColumn($col); - if (isset($col['after'])) { - $alter .= ' AFTER ' . $this->name($col['after']); - } - $colList[] = $alter; - } - break; - case 'drop': - foreach ($column as $field => $col) { - $col['name'] = $field; - $colList[] = 'DROP ' . $this->name($field); - } - break; - case 'change': - foreach ($column as $field => $col) { - if (!isset($col['name'])) { - $col['name'] = $field; - } - $alter = 'CHANGE ' . $this->name($field) . ' ' . $this->buildColumn($col); - if (isset($col['after'])) { - $alter .= ' AFTER ' . $this->name($col['after']); - } - $colList[] = $alter; - } - break; - } - } - $colList = array_merge($colList, $this->_alterIndexes($curTable, $indexes)); - $colList = array_merge($colList, $this->_alterTableParameters($curTable, $tableParameters)); - $out .= "\t" . implode(",\n\t", $colList) . ";\n\n"; - } - } - return $out; - } - -/** - * Generate a "drop table" statement for the given table - * - * @param type $table Name of the table to drop - * @return string Drop table SQL statement - */ - protected function _dropTable($table) { - return 'DROP TABLE IF EXISTS ' . $this->fullTableName($table) . ";"; - } - -/** - * Generate MySQL table parameter alteration statements for a table. - * - * @param string $table Table to alter parameters for. - * @param array $parameters Parameters to add & drop. - * @return array Array of table property alteration statements. - */ - protected function _alterTableParameters($table, $parameters) { - if (isset($parameters['change'])) { - return $this->buildTableParameters($parameters['change']); - } - return array(); - } - -/** - * Format indexes for create table - * - * @param array $indexes An array of indexes to generate SQL from - * @param string $table Optional table name, not used - * @return array An array of SQL statements for indexes - * @see DboSource::buildIndex() - */ - public function buildIndex($indexes, $table = null) { - $join = array(); - foreach ($indexes as $name => $value) { - $out = ''; - if ($name === 'PRIMARY') { - $out .= 'PRIMARY '; - $name = null; - } else { - if (!empty($value['unique'])) { - $out .= 'UNIQUE '; - } - $name = $this->startQuote . $name . $this->endQuote; - } - if (isset($value['type']) && strtolower($value['type']) === 'fulltext') { - $out .= 'FULLTEXT '; - } - $out .= 'KEY ' . $name . ' ('; - - if (is_array($value['column'])) { - if (isset($value['length'])) { - $vals = array(); - foreach ($value['column'] as $column) { - $name = $this->name($column); - if (isset($value['length'])) { - $name .= $this->_buildIndexSubPart($value['length'], $column); - } - $vals[] = $name; - } - $out .= implode(', ', $vals); - } else { - $out .= implode(', ', array_map(array(&$this, 'name'), $value['column'])); - } - } else { - $out .= $this->name($value['column']); - if (isset($value['length'])) { - $out .= $this->_buildIndexSubPart($value['length'], $value['column']); - } - } - $out .= ')'; - $join[] = $out; - } - return $join; - } - -/** - * Generate MySQL index alteration statements for a table. - * - * @param string $table Table to alter indexes for - * @param array $indexes Indexes to add and drop - * @return array Index alteration statements - */ - protected function _alterIndexes($table, $indexes) { - $alter = array(); - if (isset($indexes['drop'])) { - foreach ($indexes['drop'] as $name => $value) { - $out = 'DROP '; - if ($name === 'PRIMARY') { - $out .= 'PRIMARY KEY'; - } else { - $out .= 'KEY ' . $this->startQuote . $name . $this->endQuote; - } - $alter[] = $out; - } - } - if (isset($indexes['add'])) { - $add = $this->buildIndex($indexes['add']); - foreach ($add as $index) { - $alter[] = 'ADD ' . $index; - } - } - return $alter; - } - -/** - * Format length for text indexes - * - * @param array $lengths An array of lengths for a single index - * @param string $column The column for which to generate the index length - * @return string Formatted length part of an index field - */ - protected function _buildIndexSubPart($lengths, $column) { - if ($lengths === null) { - return ''; - } - if (!isset($lengths[$column])) { - return ''; - } - return '(' . $lengths[$column] . ')'; - } - -/** - * Returns a detailed array of sources (tables) in the database. - * - * @param string $name Table name to get parameters - * @return array Array of table names in the database - */ - public function listDetailedSources($name = null) { - $condition = ''; - if (is_string($name)) { - $condition = ' WHERE name = ' . $this->value($name); - } - $result = $this->_connection->query('SHOW TABLE STATUS ' . $condition, PDO::FETCH_ASSOC); - - if (!$result) { - $result->closeCursor(); - return array(); - } - $tables = array(); - foreach ($result as $row) { - $tables[$row['Name']] = (array)$row; - unset($tables[$row['Name']]['queryString']); - if (!empty($row['Collation'])) { - $charset = $this->getCharsetName($row['Collation']); - if ($charset) { - $tables[$row['Name']]['charset'] = $charset; - } - } - } - $result->closeCursor(); - if (is_string($name) && isset($tables[$name])) { - return $tables[$name]; - } - return $tables; - } - -/** - * Converts database-layer column types to basic types - * - * @param string $real Real database-layer column type (i.e. "varchar(255)") - * @return string Abstract column type (i.e. "string") - */ - public function column($real) { - if (is_array($real)) { - $col = $real['name']; - if (isset($real['limit'])) { - $col .= '(' . $real['limit'] . ')'; - } - return $col; - } - - $col = str_replace(')', '', $real); - $limit = $this->length($real); - if (strpos($col, '(') !== false) { - list($col, $vals) = explode('(', $col); - } - - if (in_array($col, array('date', 'time', 'datetime', 'timestamp'))) { - return $col; - } - if (($col === 'tinyint' && $limit === 1) || $col === 'boolean') { - return 'boolean'; - } - if (strpos($col, 'bigint') !== false || $col === 'bigint') { - return 'biginteger'; - } - if (strpos($col, 'tinyint') !== false) { - return 'tinyinteger'; - } - if (strpos($col, 'smallint') !== false) { - return 'smallinteger'; - } - if (strpos($col, 'int') !== false) { - return 'integer'; - } - if (strpos($col, 'char') !== false || $col === 'tinytext') { - return 'string'; - } - if (strpos($col, 'text') !== false) { - return 'text'; - } - if (strpos($col, 'blob') !== false || $col === 'binary') { - return 'binary'; - } - if (strpos($col, 'float') !== false || strpos($col, 'double') !== false) { - return 'float'; - } - if (strpos($col, 'decimal') !== false || strpos($col, 'numeric') !== false) { - return 'decimal'; - } - if (strpos($col, 'enum') !== false) { - return "enum($vals)"; - } - if (strpos($col, 'set') !== false) { - return "set($vals)"; - } - return 'text'; - } - -/** - * {@inheritDoc} - */ - public function value($data, $column = null, $null = true) { - $value = parent::value($data, $column, $null); - if (is_numeric($value) && substr($column, 0, 3) === 'set') { - return $this->_connection->quote($value); - } - return $value; - } - -/** - * Gets the schema name - * - * @return string The schema name - */ - public function getSchemaName() { - return $this->config['database']; - } - -/** - * Check if the server support nested transactions - * - * @return bool - */ - public function nestedTransactionSupported() { - return $this->useNestedTransactions && version_compare($this->getVersion(), '4.1', '>='); - } - -/** - * Check if column type is unsigned - * - * @param string $real Real database-layer column type (i.e. "varchar(255)") - * @return bool True if column is unsigned, false otherwise - */ - protected function _unsigned($real) { - return strpos(strtolower($real), 'unsigned') !== false; - } - -/** - * Inserts multiple values into a table. Uses a single query in order to insert - * multiple rows. - * - * @param string $table The table being inserted into. - * @param array $fields The array of field/column names being inserted. - * @param array $values The array of values to insert. The values should - * be an array of rows. Each row should have values keyed by the column name. - * Each row must have the values in the same order as $fields. - * @return bool - */ - public function insertMulti($table, $fields, $values) { - $table = $this->fullTableName($table); - $holder = implode(', ', array_fill(0, count($fields), '?')); - $fields = implode(', ', array_map(array($this, 'name'), $fields)); - $pdoMap = array( - 'integer' => PDO::PARAM_INT, - 'float' => PDO::PARAM_STR, - 'boolean' => PDO::PARAM_BOOL, - 'string' => PDO::PARAM_STR, - 'text' => PDO::PARAM_STR - ); - $columnMap = array(); - $rowHolder = "({$holder})"; - $sql = "INSERT INTO {$table} ({$fields}) VALUES "; - $countRows = count($values); - for ($i = 0; $i < $countRows; $i++) { - if ($i !== 0) { - $sql .= ','; - } - $sql .= " $rowHolder"; - } - $statement = $this->_connection->prepare($sql); - foreach ($values[key($values)] as $key => $val) { - $type = $this->introspectType($val); - $columnMap[$key] = $pdoMap[$type]; - } - $valuesList = array(); - $i = 1; - foreach ($values as $value) { - foreach ($value as $col => $val) { - $valuesList[] = $val; - $statement->bindValue($i, $val, $columnMap[$col]); - $i++; - } - } - $result = $statement->execute(); - $statement->closeCursor(); - if ($this->fullDebug) { - $this->logQuery($sql, $valuesList); - } - return $result; - } +class Mysql extends DboSource +{ + + /** + * Datasource description + * + * @var string + */ + public $description = "MySQL DBO Driver"; + /** + * Start quote + * + * @var string + */ + public $startQuote = "`"; + /** + * End quote + * + * @var string + */ + public $endQuote = "`"; + /** + * List of engine specific additional field parameters used on table creating + * + * @var array + */ + public $fieldParameters = [ + 'charset' => ['value' => 'CHARACTER SET', 'quote' => false, 'join' => ' ', 'column' => false, 'position' => 'beforeDefault'], + 'collate' => ['value' => 'COLLATE', 'quote' => false, 'join' => ' ', 'column' => 'Collation', 'position' => 'beforeDefault'], + 'comment' => ['value' => 'COMMENT', 'quote' => true, 'join' => ' ', 'column' => 'Comment', 'position' => 'afterDefault'], + 'unsigned' => [ + 'value' => 'UNSIGNED', + 'quote' => false, + 'join' => ' ', + 'column' => false, + 'position' => 'beforeDefault', + 'noVal' => true, + 'options' => [true], + 'types' => ['integer', 'smallinteger', 'tinyinteger', 'float', 'decimal', 'biginteger'] + ] + ]; + /** + * List of table engine specific parameters used on table creating + * + * @var array + */ + public $tableParameters = [ + 'charset' => ['value' => 'DEFAULT CHARSET', 'quote' => false, 'join' => '=', 'column' => 'charset'], + 'collate' => ['value' => 'COLLATE', 'quote' => false, 'join' => '=', 'column' => 'Collation'], + 'engine' => ['value' => 'ENGINE', 'quote' => false, 'join' => '=', 'column' => 'Engine'], + 'comment' => ['value' => 'COMMENT', 'quote' => true, 'join' => '=', 'column' => 'Comment'], + ]; + /** + * MySQL column definition + * + * @var array + * @link https://dev.mysql.com/doc/refman/5.7/en/data-types.html MySQL Data Types + */ + public $columns = [ + 'primary_key' => ['name' => 'NOT NULL AUTO_INCREMENT'], + 'string' => ['name' => 'varchar', 'limit' => '255'], + 'text' => ['name' => 'text'], + 'enum' => ['name' => 'enum'], + 'biginteger' => ['name' => 'bigint', 'limit' => '20'], + 'integer' => ['name' => 'int', 'limit' => '11', 'formatter' => 'intval'], + 'smallinteger' => ['name' => 'smallint', 'limit' => '6', 'formatter' => 'intval'], + 'tinyinteger' => ['name' => 'tinyint', 'limit' => '4', 'formatter' => 'intval'], + 'float' => ['name' => 'float', 'formatter' => 'floatval'], + 'decimal' => ['name' => 'decimal', 'formatter' => 'floatval'], + 'datetime' => ['name' => 'datetime', 'format' => 'Y-m-d H:i:s', 'formatter' => 'date'], + 'timestamp' => ['name' => 'timestamp', 'format' => 'Y-m-d H:i:s', 'formatter' => 'date'], + 'time' => ['name' => 'time', 'format' => 'H:i:s', 'formatter' => 'date'], + 'date' => ['name' => 'date', 'format' => 'Y-m-d', 'formatter' => 'date'], + 'binary' => ['name' => 'blob'], + 'boolean' => ['name' => 'tinyint', 'limit' => '1'] + ]; + /** + * Base configuration settings for MySQL driver + * + * @var array + */ + protected $_baseConfig = [ + 'persistent' => true, + 'host' => 'localhost', + 'login' => 'root', + 'password' => '', + 'database' => 'cake', + 'port' => '3306', + 'flags' => [] + ]; + /** + * Reference to the PDO object connection + * + * @var PDO + */ + protected $_connection = null; + /** + * use alias for update and delete. Set to true if version >= 4.1 + * + * @var bool + */ + protected $_useAlias = true; + /** + * Mapping of collation names to character set names + * + * @var array + */ + protected $_charsets = []; + + /** + * Connects to the database using options in the given configuration array. + * + * MySQL supports a few additional options that other drivers do not: + * + * - `unix_socket` Set to the path of the MySQL sock file. Can be used in place + * of host + port. + * - `ssl_key` SSL key file for connecting via SSL. Must be combined with `ssl_cert`. + * - `ssl_cert` The SSL certificate to use when connecting via SSL. Must be + * combined with `ssl_key`. + * - `ssl_ca` The certificate authority for SSL connections. + * + * @return bool True if the database could be connected, else false + * @throws MissingConnectionException + */ + public function connect() + { + $config = $this->config; + $this->connected = false; + + $flags = $config['flags'] + [ + PDO::ATTR_PERSISTENT => $config['persistent'], + PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => true, + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION + ]; + + if (!empty($config['encoding'])) { + $flags[PDO::MYSQL_ATTR_INIT_COMMAND] = 'SET NAMES ' . $config['encoding']; + } + if (!empty($config['ssl_key']) && !empty($config['ssl_cert'])) { + $flags[PDO::MYSQL_ATTR_SSL_KEY] = $config['ssl_key']; + $flags[PDO::MYSQL_ATTR_SSL_CERT] = $config['ssl_cert']; + } + if (!empty($config['ssl_ca'])) { + $flags[PDO::MYSQL_ATTR_SSL_CA] = $config['ssl_ca']; + } + if (empty($config['unix_socket'])) { + $dsn = "mysql:host={$config['host']};port={$config['port']};dbname={$config['database']}"; + } else { + $dsn = "mysql:unix_socket={$config['unix_socket']};dbname={$config['database']}"; + } + + try { + $this->_connection = new PDO( + $dsn, + $config['login'], + $config['password'], + $flags + ); + $this->connected = true; + if (!empty($config['settings'])) { + foreach ($config['settings'] as $key => $value) { + $this->_execute("SET $key=$value"); + } + } + } catch (PDOException $e) { + throw new MissingConnectionException([ + 'class' => get_class($this), + 'message' => $e->getMessage() + ]); + } + + $this->_charsets = []; + $this->_useAlias = (bool)version_compare($this->getVersion(), "4.1", ">="); + + return $this->connected; + } + + /** + * Check whether the MySQL extension is installed/loaded + * + * @return bool + */ + public function enabled() + { + return in_array('mysql', PDO::getAvailableDrivers()); + } + + /** + * Returns an array of sources (tables) in the database. + * + * @param mixed $data List of tables. + * @return array Array of table names in the database + */ + public function listSources($data = null) + { + $cache = parent::listSources(); + if ($cache) { + return $cache; + } + $result = $this->_execute('SHOW TABLES FROM ' . $this->name($this->config['database'])); + + if (!$result) { + $result->closeCursor(); + return []; + } + $tables = []; + + while ($line = $result->fetch(PDO::FETCH_NUM)) { + $tables[] = $line[0]; + } + + $result->closeCursor(); + parent::listSources($tables); + return $tables; + } + + /** + * Builds a map of the columns contained in a result + * + * @param PDOStatement $results The results to format. + * @return void + */ + public function resultSet($results) + { + $this->map = []; + $numFields = $results->columnCount(); + $index = 0; + + while ($numFields-- > 0) { + $column = $results->getColumnMeta($index); + if ($column['len'] === 1 && (empty($column['native_type']) || $column['native_type'] === 'TINY')) { + $type = 'boolean'; + } else { + $type = empty($column['native_type']) ? 'string' : $column['native_type']; + } + if (!empty($column['table']) && strpos($column['name'], $this->virtualFieldSeparator) === false) { + $this->map[$index++] = [$column['table'], $column['name'], $type]; + } else { + $this->map[$index++] = [0, $column['name'], $type]; + } + } + } + + /** + * Fetches the next row from the current result set + * + * @return mixed array with results fetched and mapped to column names or false if there is no results left to fetch + */ + public function fetchResult() + { + if ($row = $this->_result->fetch(PDO::FETCH_NUM)) { + $resultRow = []; + foreach ($this->map as $col => $meta) { + list($table, $column, $type) = $meta; + $resultRow[$table][$column] = $row[$col]; + if ($type === 'boolean' && $row[$col] !== null) { + $resultRow[$table][$column] = $this->boolean($resultRow[$table][$column]); + } + } + return $resultRow; + } + $this->_result->closeCursor(); + return false; + } + + /** + * Gets the database encoding + * + * @return string The database encoding + */ + public function getEncoding() + { + return $this->_execute('SHOW VARIABLES LIKE ?', ['character_set_client'])->fetchObject()->Value; + } + + /** + * Generates and executes an SQL UPDATE statement for given model, fields, and values. + * + * @param Model $model The model to update. + * @param array $fields The fields to update. + * @param array $values The values to set. + * @param mixed $conditions The conditions to use. + * @return bool + */ + public function update(Model $model, $fields = [], $values = null, $conditions = null) + { + if (!$this->_useAlias) { + return parent::update($model, $fields, $values, $conditions); + } + + if (!$values) { + $combined = $fields; + } else { + $combined = array_combine($fields, $values); + } + + $alias = $joins = false; + $fields = $this->_prepareUpdateFields($model, $combined, empty($conditions), !empty($conditions)); + $fields = implode(', ', $fields); + $table = $this->fullTableName($model); + + if (!empty($conditions)) { + $alias = $this->name($model->alias); + if ($model->name === $model->alias) { + $joins = implode(' ', $this->_getJoins($model)); + } + } + $conditions = $this->conditions($this->defaultConditions($model, $conditions, $alias), true, true, $model); + + if ($conditions === false) { + return false; + } + + if (!$this->execute($this->renderStatement('update', compact('table', 'alias', 'joins', 'fields', 'conditions')))) { + $model->onError(); + return false; + } + return true; + } + + /** + * Generates and executes an SQL DELETE statement for given id/conditions on given model. + * + * @param Model $model The model to delete from. + * @param mixed $conditions The conditions to use. + * @return bool Success + */ + public function delete(Model $model, $conditions = null) + { + if (!$this->_useAlias) { + return parent::delete($model, $conditions); + } + $alias = $this->name($model->alias); + $table = $this->fullTableName($model); + $joins = implode(' ', $this->_getJoins($model)); + + if (empty($conditions)) { + $alias = $joins = false; + } + $complexConditions = $this->_deleteNeedsComplexConditions($model, $conditions); + if (!$complexConditions) { + $joins = false; + } + + $conditions = $this->conditions($this->defaultConditions($model, $conditions, $alias), true, true, $model); + if ($conditions === false) { + return false; + } + if ($this->execute($this->renderStatement('delete', compact('alias', 'table', 'joins', 'conditions'))) === false) { + $model->onError(); + return false; + } + return true; + } + + /** + * Checks whether complex conditions are needed for a delete with the given conditions. + * + * @param Model $model The model to delete from. + * @param mixed $conditions The conditions to use. + * @return bool Whether or not complex conditions are needed + */ + protected function _deleteNeedsComplexConditions(Model $model, $conditions) + { + $fields = array_keys($this->describe($model)); + foreach ((array)$conditions as $key => $value) { + if (in_array(strtolower(trim($key)), $this->_sqlBoolOps, true)) { + if ($this->_deleteNeedsComplexConditions($model, $value)) { + return true; + } + } else if (strpos($key, $model->alias) === false && !in_array($key, $fields, true)) { + return true; + } + } + return false; + } + + /** + * Returns an array of the fields in given table name. + * + * @param Model|string $model Name of database table to inspect or model instance + * @return array Fields in table. Keys are name and type + * @throws CakeException + */ + public function describe($model) + { + $key = $this->fullTableName($model, false); + $cache = parent::describe($key); + if ($cache) { + return $cache; + } + $table = $this->fullTableName($model); + + $fields = false; + $cols = $this->_execute('SHOW FULL COLUMNS FROM ' . $table); + if (!$cols) { + throw new CakeException(__d('cake_dev', 'Could not describe table for %s', $table)); + } + + while ($column = $cols->fetch(PDO::FETCH_OBJ)) { + $fields[$column->Field] = [ + 'type' => $this->column($column->Type), + 'null' => ($column->Null === 'YES' ? true : false), + 'default' => $column->Default, + 'length' => $this->length($column->Type) + ]; + if (in_array($fields[$column->Field]['type'], $this->fieldParameters['unsigned']['types'], true)) { + $fields[$column->Field]['unsigned'] = $this->_unsigned($column->Type); + } + if (in_array($fields[$column->Field]['type'], ['timestamp', 'datetime']) && + in_array(strtoupper($column->Default), ['CURRENT_TIMESTAMP', 'CURRENT_TIMESTAMP()']) + ) { + $fields[$column->Field]['default'] = null; + } + if (!empty($column->Key) && isset($this->index[$column->Key])) { + $fields[$column->Field]['key'] = $this->index[$column->Key]; + } + foreach ($this->fieldParameters as $name => $value) { + if (!empty($column->{$value['column']})) { + $fields[$column->Field][$name] = $column->{$value['column']}; + } + } + if (isset($fields[$column->Field]['collate'])) { + $charset = $this->getCharsetName($fields[$column->Field]['collate']); + if ($charset) { + $fields[$column->Field]['charset'] = $charset; + } + } + } + $this->_cacheDescription($key, $fields); + $cols->closeCursor(); + return $fields; + } + + /** + * Converts database-layer column types to basic types + * + * @param string $real Real database-layer column type (i.e. "varchar(255)") + * @return string Abstract column type (i.e. "string") + */ + public function column($real) + { + if (is_array($real)) { + $col = $real['name']; + if (isset($real['limit'])) { + $col .= '(' . $real['limit'] . ')'; + } + return $col; + } + + $col = str_replace(')', '', $real); + $limit = $this->length($real); + if (strpos($col, '(') !== false) { + list($col, $vals) = explode('(', $col); + } + + if (in_array($col, ['date', 'time', 'datetime', 'timestamp'])) { + return $col; + } + if (($col === 'tinyint' && $limit === 1) || $col === 'boolean') { + return 'boolean'; + } + if (strpos($col, 'bigint') !== false || $col === 'bigint') { + return 'biginteger'; + } + if (strpos($col, 'tinyint') !== false) { + return 'tinyinteger'; + } + if (strpos($col, 'smallint') !== false) { + return 'smallinteger'; + } + if (strpos($col, 'int') !== false) { + return 'integer'; + } + if (strpos($col, 'char') !== false || $col === 'tinytext') { + return 'string'; + } + if (strpos($col, 'text') !== false) { + return 'text'; + } + if (strpos($col, 'blob') !== false || $col === 'binary') { + return 'binary'; + } + if (strpos($col, 'float') !== false || strpos($col, 'double') !== false) { + return 'float'; + } + if (strpos($col, 'decimal') !== false || strpos($col, 'numeric') !== false) { + return 'decimal'; + } + if (strpos($col, 'enum') !== false) { + return "enum($vals)"; + } + if (strpos($col, 'set') !== false) { + return "set($vals)"; + } + return 'text'; + } + + /** + * Check if column type is unsigned + * + * @param string $real Real database-layer column type (i.e. "varchar(255)") + * @return bool True if column is unsigned, false otherwise + */ + protected function _unsigned($real) + { + return strpos(strtolower($real), 'unsigned') !== false; + } + + /** + * Query charset by collation + * + * @param string $name Collation name + * @return string|false Character set name + */ + public function getCharsetName($name) + { + if ((bool)version_compare($this->getVersion(), "5", "<")) { + return false; + } + if (isset($this->_charsets[$name])) { + return $this->_charsets[$name]; + } + $r = $this->_execute( + 'SELECT CHARACTER_SET_NAME FROM INFORMATION_SCHEMA.COLLATIONS WHERE COLLATION_NAME = ?', + [$name] + ); + $cols = $r->fetch(PDO::FETCH_ASSOC); + + if (isset($cols['CHARACTER_SET_NAME'])) { + $this->_charsets[$name] = $cols['CHARACTER_SET_NAME']; + } else { + $this->_charsets[$name] = false; + } + return $this->_charsets[$name]; + } + + /** + * Sets the database encoding + * + * @param string $enc Database encoding + * @return bool + */ + public function setEncoding($enc) + { + return $this->_execute('SET NAMES ' . $enc) !== false; + } + + /** + * Returns an array of the indexes in given datasource name. + * + * @param string $model Name of model to inspect + * @return array Fields in table. Keys are column and unique + */ + public function index($model) + { + $index = []; + $table = $this->fullTableName($model); + $old = version_compare($this->getVersion(), '4.1', '<='); + if ($table) { + $indexes = $this->_execute('SHOW INDEX FROM ' . $table); + // @codingStandardsIgnoreStart + // MySQL columns don't match the cakephp conventions. + while ($idx = $indexes->fetch(PDO::FETCH_OBJ)) { + if ($old) { + $idx = (object)current((array)$idx); + } + if (!isset($index[$idx->Key_name]['column'])) { + $col = []; + $index[$idx->Key_name]['column'] = $idx->Column_name; + + if ($idx->Index_type === 'FULLTEXT') { + $index[$idx->Key_name]['type'] = strtolower($idx->Index_type); + } else { + $index[$idx->Key_name]['unique'] = (int)($idx->Non_unique == 0); + } + } else { + if (!empty($index[$idx->Key_name]['column']) && !is_array($index[$idx->Key_name]['column'])) { + $col[] = $index[$idx->Key_name]['column']; + } + $col[] = $idx->Column_name; + $index[$idx->Key_name]['column'] = $col; + } + if (!empty($idx->Sub_part)) { + if (!isset($index[$idx->Key_name]['length'])) { + $index[$idx->Key_name]['length'] = []; + } + $index[$idx->Key_name]['length'][$idx->Column_name] = $idx->Sub_part; + } + } + // @codingStandardsIgnoreEnd + $indexes->closeCursor(); + } + return $index; + } + + /** + * Generate a MySQL Alter Table syntax for the given Schema comparison + * + * @param array $compare Result of a CakeSchema::compare() + * @param string $table The table name. + * @return string|false String of alter statements to make. + */ + public function alterSchema($compare, $table = null) + { + if (!is_array($compare)) { + return false; + } + $out = ''; + $colList = []; + foreach ($compare as $curTable => $types) { + $indexes = $tableParameters = $colList = []; + if (!$table || $table === $curTable) { + $out .= 'ALTER TABLE ' . $this->fullTableName($curTable) . " \n"; + foreach ($types as $type => $column) { + if (isset($column['indexes'])) { + $indexes[$type] = $column['indexes']; + unset($column['indexes']); + } + if (isset($column['tableParameters'])) { + $tableParameters[$type] = $column['tableParameters']; + unset($column['tableParameters']); + } + switch ($type) { + case 'add': + foreach ($column as $field => $col) { + $col['name'] = $field; + $alter = 'ADD ' . $this->buildColumn($col); + if (isset($col['after'])) { + $alter .= ' AFTER ' . $this->name($col['after']); + } + $colList[] = $alter; + } + break; + case 'drop': + foreach ($column as $field => $col) { + $col['name'] = $field; + $colList[] = 'DROP ' . $this->name($field); + } + break; + case 'change': + foreach ($column as $field => $col) { + if (!isset($col['name'])) { + $col['name'] = $field; + } + $alter = 'CHANGE ' . $this->name($field) . ' ' . $this->buildColumn($col); + if (isset($col['after'])) { + $alter .= ' AFTER ' . $this->name($col['after']); + } + $colList[] = $alter; + } + break; + } + } + $colList = array_merge($colList, $this->_alterIndexes($curTable, $indexes)); + $colList = array_merge($colList, $this->_alterTableParameters($curTable, $tableParameters)); + $out .= "\t" . implode(",\n\t", $colList) . ";\n\n"; + } + } + return $out; + } + + /** + * Generate MySQL index alteration statements for a table. + * + * @param string $table Table to alter indexes for + * @param array $indexes Indexes to add and drop + * @return array Index alteration statements + */ + protected function _alterIndexes($table, $indexes) + { + $alter = []; + if (isset($indexes['drop'])) { + foreach ($indexes['drop'] as $name => $value) { + $out = 'DROP '; + if ($name === 'PRIMARY') { + $out .= 'PRIMARY KEY'; + } else { + $out .= 'KEY ' . $this->startQuote . $name . $this->endQuote; + } + $alter[] = $out; + } + } + if (isset($indexes['add'])) { + $add = $this->buildIndex($indexes['add']); + foreach ($add as $index) { + $alter[] = 'ADD ' . $index; + } + } + return $alter; + } + + /** + * Format indexes for create table + * + * @param array $indexes An array of indexes to generate SQL from + * @param string $table Optional table name, not used + * @return array An array of SQL statements for indexes + * @see DboSource::buildIndex() + */ + public function buildIndex($indexes, $table = null) + { + $join = []; + foreach ($indexes as $name => $value) { + $out = ''; + if ($name === 'PRIMARY') { + $out .= 'PRIMARY '; + $name = null; + } else { + if (!empty($value['unique'])) { + $out .= 'UNIQUE '; + } + $name = $this->startQuote . $name . $this->endQuote; + } + if (isset($value['type']) && strtolower($value['type']) === 'fulltext') { + $out .= 'FULLTEXT '; + } + $out .= 'KEY ' . $name . ' ('; + + if (is_array($value['column'])) { + if (isset($value['length'])) { + $vals = []; + foreach ($value['column'] as $column) { + $name = $this->name($column); + if (isset($value['length'])) { + $name .= $this->_buildIndexSubPart($value['length'], $column); + } + $vals[] = $name; + } + $out .= implode(', ', $vals); + } else { + $out .= implode(', ', array_map([&$this, 'name'], $value['column'])); + } + } else { + $out .= $this->name($value['column']); + if (isset($value['length'])) { + $out .= $this->_buildIndexSubPart($value['length'], $value['column']); + } + } + $out .= ')'; + $join[] = $out; + } + return $join; + } + + /** + * Format length for text indexes + * + * @param array $lengths An array of lengths for a single index + * @param string $column The column for which to generate the index length + * @return string Formatted length part of an index field + */ + protected function _buildIndexSubPart($lengths, $column) + { + if ($lengths === null) { + return ''; + } + if (!isset($lengths[$column])) { + return ''; + } + return '(' . $lengths[$column] . ')'; + } + + /** + * Generate MySQL table parameter alteration statements for a table. + * + * @param string $table Table to alter parameters for. + * @param array $parameters Parameters to add & drop. + * @return array Array of table property alteration statements. + */ + protected function _alterTableParameters($table, $parameters) + { + if (isset($parameters['change'])) { + return $this->buildTableParameters($parameters['change']); + } + return []; + } + + /** + * Returns a detailed array of sources (tables) in the database. + * + * @param string $name Table name to get parameters + * @return array Array of table names in the database + */ + public function listDetailedSources($name = null) + { + $condition = ''; + if (is_string($name)) { + $condition = ' WHERE name = ' . $this->value($name); + } + $result = $this->_connection->query('SHOW TABLE STATUS ' . $condition, PDO::FETCH_ASSOC); + + if (!$result) { + $result->closeCursor(); + return []; + } + $tables = []; + foreach ($result as $row) { + $tables[$row['Name']] = (array)$row; + unset($tables[$row['Name']]['queryString']); + if (!empty($row['Collation'])) { + $charset = $this->getCharsetName($row['Collation']); + if ($charset) { + $tables[$row['Name']]['charset'] = $charset; + } + } + } + $result->closeCursor(); + if (is_string($name) && isset($tables[$name])) { + return $tables[$name]; + } + return $tables; + } + + /** + * {@inheritDoc} + */ + public function value($data, $column = null, $null = true) + { + $value = parent::value($data, $column, $null); + if (is_numeric($value) && substr($column, 0, 3) === 'set') { + return $this->_connection->quote($value); + } + return $value; + } + + /** + * Gets the schema name + * + * @return string The schema name + */ + public function getSchemaName() + { + return $this->config['database']; + } + + /** + * Check if the server support nested transactions + * + * @return bool + */ + public function nestedTransactionSupported() + { + return $this->useNestedTransactions && version_compare($this->getVersion(), '4.1', '>='); + } + + /** + * Inserts multiple values into a table. Uses a single query in order to insert + * multiple rows. + * + * @param string $table The table being inserted into. + * @param array $fields The array of field/column names being inserted. + * @param array $values The array of values to insert. The values should + * be an array of rows. Each row should have values keyed by the column name. + * Each row must have the values in the same order as $fields. + * @return bool + */ + public function insertMulti($table, $fields, $values) + { + $table = $this->fullTableName($table); + $holder = implode(', ', array_fill(0, count($fields), '?')); + $fields = implode(', ', array_map([$this, 'name'], $fields)); + $pdoMap = [ + 'integer' => PDO::PARAM_INT, + 'float' => PDO::PARAM_STR, + 'boolean' => PDO::PARAM_BOOL, + 'string' => PDO::PARAM_STR, + 'text' => PDO::PARAM_STR + ]; + $columnMap = []; + $rowHolder = "({$holder})"; + $sql = "INSERT INTO {$table} ({$fields}) VALUES "; + $countRows = count($values); + for ($i = 0; $i < $countRows; $i++) { + if ($i !== 0) { + $sql .= ','; + } + $sql .= " $rowHolder"; + } + $statement = $this->_connection->prepare($sql); + foreach ($values[key($values)] as $key => $val) { + $type = $this->introspectType($val); + $columnMap[$key] = $pdoMap[$type]; + } + $valuesList = []; + $i = 1; + foreach ($values as $value) { + foreach ($value as $col => $val) { + $valuesList[] = $val; + $statement->bindValue($i, $val, $columnMap[$col]); + $i++; + } + } + $result = $statement->execute(); + $statement->closeCursor(); + if ($this->fullDebug) { + $this->logQuery($sql, $valuesList); + } + return $result; + } + + /** + * Generate a "drop table" statement for the given table + * + * @param type $table Name of the table to drop + * @return string Drop table SQL statement + */ + protected function _dropTable($table) + { + return 'DROP TABLE IF EXISTS ' . $this->fullTableName($table) . ";"; + } } diff --git a/lib/Cake/Model/Datasource/Database/Postgres.php b/lib/Cake/Model/Datasource/Database/Postgres.php index f81bee5b..0455ce91 100755 --- a/lib/Cake/Model/Datasource/Database/Postgres.php +++ b/lib/Cake/Model/Datasource/Database/Postgres.php @@ -21,191 +21,248 @@ * * @package Cake.Model.Datasource.Database */ -class Postgres extends DboSource { - -/** - * Driver description - * - * @var string - */ - public $description = "PostgreSQL DBO Driver"; - -/** - * Base driver configuration settings. Merged with user settings. - * - * @var array - */ - protected $_baseConfig = array( - 'persistent' => true, - 'host' => 'localhost', - 'login' => 'root', - 'password' => '', - 'database' => 'cake', - 'schema' => 'public', - 'port' => 5432, - 'encoding' => '', - 'sslmode' => 'allow', - 'flags' => array() - ); - -/** - * Columns - * - * @var array - * @link https://www.postgresql.org/docs/9.6/static/datatype.html PostgreSQL Data Types - */ - public $columns = array( - 'primary_key' => array('name' => 'serial NOT NULL'), - 'string' => array('name' => 'varchar', 'limit' => '255'), - 'text' => array('name' => 'text'), - 'integer' => array('name' => 'integer', 'formatter' => 'intval'), - 'smallinteger' => array('name' => 'smallint', 'formatter' => 'intval'), - 'tinyinteger' => array('name' => 'smallint', 'formatter' => 'intval'), - 'biginteger' => array('name' => 'bigint', 'limit' => '20'), - 'float' => array('name' => 'float', 'formatter' => 'floatval'), - 'decimal' => array('name' => 'decimal', 'formatter' => 'floatval'), - 'datetime' => array('name' => 'timestamp', 'format' => 'Y-m-d H:i:s', 'formatter' => 'date'), - 'timestamp' => array('name' => 'timestamp', 'format' => 'Y-m-d H:i:s', 'formatter' => 'date'), - 'time' => array('name' => 'time', 'format' => 'H:i:s', 'formatter' => 'date'), - 'date' => array('name' => 'date', 'format' => 'Y-m-d', 'formatter' => 'date'), - 'binary' => array('name' => 'bytea'), - 'boolean' => array('name' => 'boolean'), - 'number' => array('name' => 'numeric'), - 'inet' => array('name' => 'inet'), - 'uuid' => array('name' => 'uuid') - ); - -/** - * Starting Quote - * - * @var string - */ - public $startQuote = '"'; - -/** - * Ending Quote - * - * @var string - */ - public $endQuote = '"'; - -/** - * Contains mappings of custom auto-increment sequences, if a table uses a sequence name - * other than what is dictated by convention. - * - * @var array - */ - protected $_sequenceMap = array(); - -/** - * The set of valid SQL operations usable in a WHERE statement - * - * @var array - */ - protected $_sqlOps = array('like', 'ilike', 'or', 'not', 'in', 'between', '~', '~\*', '\!~', '\!~\*', 'similar to'); - -/** - * Connects to the database using options in the given configuration array. - * - * @return bool True if successfully connected. - * @throws MissingConnectionException - */ - public function connect() { - $config = $this->config; - $this->connected = false; - - $flags = $config['flags'] + array( - PDO::ATTR_PERSISTENT => $config['persistent'], - PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION - ); - - try { - $this->_connection = new PDO( - "pgsql:host={$config['host']};port={$config['port']};dbname={$config['database']};sslmode={$config['sslmode']}", - $config['login'], - $config['password'], - $flags - ); - - $this->connected = true; - if (!empty($config['encoding'])) { - $this->setEncoding($config['encoding']); - } - if (!empty($config['schema'])) { - $this->_execute('SET search_path TO "' . $config['schema'] . '"'); - } - if (!empty($config['settings'])) { - foreach ($config['settings'] as $key => $value) { - $this->_execute("SET $key TO $value"); - } - } - } catch (PDOException $e) { - throw new MissingConnectionException(array( - 'class' => get_class($this), - 'message' => $e->getMessage() - )); - } - - return $this->connected; - } - -/** - * Check if PostgreSQL is enabled/loaded - * - * @return bool - */ - public function enabled() { - return in_array('pgsql', PDO::getAvailableDrivers()); - } - -/** - * Returns an array of tables in the database. If there are no tables, an error is raised and the application exits. - * - * @param mixed $data The sources to list. - * @return array Array of table names in the database - */ - public function listSources($data = null) { - $cache = parent::listSources(); - - if ($cache) { - return $cache; - } - - $schema = $this->config['schema']; - $sql = "SELECT table_name as name FROM INFORMATION_SCHEMA.tables WHERE table_schema = ?"; - $result = $this->_execute($sql, array($schema)); - - if (!$result) { - return array(); - } - - $tables = array(); - - foreach ($result as $item) { - $tables[] = $item->name; - } - - $result->closeCursor(); - parent::listSources($tables); - return $tables; - } - -/** - * Returns an array of the fields in given table name. - * - * @param Model|string $model Name of database table to inspect - * @return array Fields in table. Keys are name and type - */ - public function describe($model) { - $table = $this->fullTableName($model, false, false); - $fields = parent::describe($table); - $this->_sequenceMap[$table] = array(); - $cols = null; - $hasPrimary = false; - - if ($fields === null) { - $cols = $this->_execute( - 'SELECT DISTINCT table_schema AS schema, +class Postgres extends DboSource +{ + + /** + * Driver description + * + * @var string + */ + public $description = "PostgreSQL DBO Driver"; + /** + * Columns + * + * @var array + * @link https://www.postgresql.org/docs/9.6/static/datatype.html PostgreSQL Data Types + */ + public $columns = [ + 'primary_key' => ['name' => 'serial NOT NULL'], + 'string' => ['name' => 'varchar', 'limit' => '255'], + 'text' => ['name' => 'text'], + 'integer' => ['name' => 'integer', 'formatter' => 'intval'], + 'smallinteger' => ['name' => 'smallint', 'formatter' => 'intval'], + 'tinyinteger' => ['name' => 'smallint', 'formatter' => 'intval'], + 'biginteger' => ['name' => 'bigint', 'limit' => '20'], + 'float' => ['name' => 'float', 'formatter' => 'floatval'], + 'decimal' => ['name' => 'decimal', 'formatter' => 'floatval'], + 'datetime' => ['name' => 'timestamp', 'format' => 'Y-m-d H:i:s', 'formatter' => 'date'], + 'timestamp' => ['name' => 'timestamp', 'format' => 'Y-m-d H:i:s', 'formatter' => 'date'], + 'time' => ['name' => 'time', 'format' => 'H:i:s', 'formatter' => 'date'], + 'date' => ['name' => 'date', 'format' => 'Y-m-d', 'formatter' => 'date'], + 'binary' => ['name' => 'bytea'], + 'boolean' => ['name' => 'boolean'], + 'number' => ['name' => 'numeric'], + 'inet' => ['name' => 'inet'], + 'uuid' => ['name' => 'uuid'] + ]; + /** + * Starting Quote + * + * @var string + */ + public $startQuote = '"'; + /** + * Ending Quote + * + * @var string + */ + public $endQuote = '"'; + /** + * Base driver configuration settings. Merged with user settings. + * + * @var array + */ + protected $_baseConfig = [ + 'persistent' => true, + 'host' => 'localhost', + 'login' => 'root', + 'password' => '', + 'database' => 'cake', + 'schema' => 'public', + 'port' => 5432, + 'encoding' => '', + 'sslmode' => 'allow', + 'flags' => [] + ]; + /** + * Contains mappings of custom auto-increment sequences, if a table uses a sequence name + * other than what is dictated by convention. + * + * @var array + */ + protected $_sequenceMap = []; + + /** + * The set of valid SQL operations usable in a WHERE statement + * + * @var array + */ + protected $_sqlOps = ['like', 'ilike', 'or', 'not', 'in', 'between', '~', '~\*', '\!~', '\!~\*', 'similar to']; + + /** + * Connects to the database using options in the given configuration array. + * + * @return bool True if successfully connected. + * @throws MissingConnectionException + */ + public function connect() + { + $config = $this->config; + $this->connected = false; + + $flags = $config['flags'] + [ + PDO::ATTR_PERSISTENT => $config['persistent'], + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION + ]; + + try { + $this->_connection = new PDO( + "pgsql:host={$config['host']};port={$config['port']};dbname={$config['database']};sslmode={$config['sslmode']}", + $config['login'], + $config['password'], + $flags + ); + + $this->connected = true; + if (!empty($config['encoding'])) { + $this->setEncoding($config['encoding']); + } + if (!empty($config['schema'])) { + $this->_execute('SET search_path TO "' . $config['schema'] . '"'); + } + if (!empty($config['settings'])) { + foreach ($config['settings'] as $key => $value) { + $this->_execute("SET $key TO $value"); + } + } + } catch (PDOException $e) { + throw new MissingConnectionException([ + 'class' => get_class($this), + 'message' => $e->getMessage() + ]); + } + + return $this->connected; + } + + /** + * Sets the database encoding + * + * @param mixed $enc Database encoding + * @return bool True on success, false on failure + */ + public function setEncoding($enc) + { + return $this->_execute('SET NAMES ' . $this->value($enc)) !== false; + } + + /** + * {@inheritDoc} + */ + public function value($data, $column = null, $null = true) + { + $value = parent::value($data, $column, $null); + if ($column === 'uuid' && is_scalar($data) && $data === '') { + return 'NULL'; + } + return $value; + } + + /** + * Check if PostgreSQL is enabled/loaded + * + * @return bool + */ + public function enabled() + { + return in_array('pgsql', PDO::getAvailableDrivers()); + } + + /** + * Returns an array of tables in the database. If there are no tables, an error is raised and the application exits. + * + * @param mixed $data The sources to list. + * @return array Array of table names in the database + */ + public function listSources($data = null) + { + $cache = parent::listSources(); + + if ($cache) { + return $cache; + } + + $schema = $this->config['schema']; + $sql = "SELECT table_name as name FROM INFORMATION_SCHEMA.tables WHERE table_schema = ?"; + $result = $this->_execute($sql, [$schema]); + + if (!$result) { + return []; + } + + $tables = []; + + foreach ($result as $item) { + $tables[] = $item->name; + } + + $result->closeCursor(); + parent::listSources($tables); + return $tables; + } + + /** + * Returns the ID generated from the previous INSERT operation. + * + * @param string $source Name of the database table + * @param string $field Name of the ID database field. Defaults to "id" + * @return int + */ + public function lastInsertId($source = null, $field = 'id') + { + $seq = $this->getSequence($source, $field); + return $this->_connection->lastInsertId($seq); + } + + /** + * Gets the associated sequence for the given table/field + * + * @param string|Model $table Either a full table name (with prefix) as a string, or a model object + * @param string $field Name of the ID database field. Defaults to "id" + * @return string The associated sequence name from the sequence map, defaults to "{$table}_{$field}_seq" + */ + public function getSequence($table, $field = 'id') + { + if (is_object($table)) { + $table = $this->fullTableName($table, false, false); + } + if (!isset($this->_sequenceMap[$table])) { + $this->describe($table); + } + if (isset($this->_sequenceMap[$table][$field])) { + return $this->_sequenceMap[$table][$field]; + } + return "{$table}_{$field}_seq"; + } + + /** + * Returns an array of the fields in given table name. + * + * @param Model|string $model Name of database table to inspect + * @return array Fields in table. Keys are name and type + */ + public function describe($model) + { + $table = $this->fullTableName($model, false, false); + $fields = parent::describe($table); + $this->_sequenceMap[$table] = []; + $cols = null; + $hasPrimary = false; + + if ($fields === null) { + $cols = $this->_execute( + 'SELECT DISTINCT table_schema AS schema, column_name AS name, data_type AS type, is_nullable AS null, @@ -220,284 +277,305 @@ public function describe($model) { LEFT JOIN pg_catalog.pg_attribute attr ON (cl.oid = attr.attrelid AND column_name = attr.attname) WHERE table_name = ? AND table_schema = ? AND table_catalog = ? ORDER BY ordinal_position', - array($table, $this->config['schema'], $this->config['database']) - ); - - // @codingStandardsIgnoreStart - // Postgres columns don't match the coding standards. - foreach ($cols as $c) { - $type = $c->type; - if (!empty($c->oct_length) && $c->char_length === null) { - if ($c->type === 'character varying') { - $length = null; - $type = 'text'; - } elseif ($c->type === 'uuid') { - $type = 'uuid'; - $length = 36; - } else { - $length = (int)$c->oct_length; - } - } elseif (!empty($c->char_length)) { - $length = (int)$c->char_length; - } else { - $length = $this->length($c->type); - } - if (empty($length)) { - $length = null; - } - $fields[$c->name] = array( - 'type' => $this->column($type), - 'null' => ($c->null === 'NO' ? false : true), - 'default' => preg_replace( - "/^'(.*)'$/", - "$1", - preg_replace('/::[\w\s]+/', '', $c->default) - ), - 'length' => $length, - ); - - // Serial columns are primary integer keys - if ($c->has_serial) { - $fields[$c->name]['key'] = 'primary'; - $fields[$c->name]['length'] = 11; - $hasPrimary = true; - } - if ($hasPrimary === false && - $model instanceof Model && - $c->name === $model->primaryKey - ) { - $fields[$c->name]['key'] = 'primary'; - if ( - $fields[$c->name]['type'] !== 'string' && - $fields[$c->name]['type'] !== 'uuid' - ) { - $fields[$c->name]['length'] = 11; - } - } - if ( - $fields[$c->name]['default'] === 'NULL' || - $c->default === null || - preg_match('/nextval\([\'"]?([\w.]+)/', $c->default, $seq) - ) { - $fields[$c->name]['default'] = null; - if (!empty($seq) && isset($seq[1])) { - if (strpos($seq[1], '.') === false) { - $sequenceName = $c->schema . '.' . $seq[1]; - } else { - $sequenceName = $seq[1]; - } - $this->_sequenceMap[$table][$c->name] = $sequenceName; - } - } - if ($fields[$c->name]['type'] === 'timestamp' && $fields[$c->name]['default'] === '') { - $fields[$c->name]['default'] = null; - } - if ($fields[$c->name]['type'] === 'boolean' && !empty($fields[$c->name]['default'])) { - $fields[$c->name]['default'] = constant($fields[$c->name]['default']); - } - } - $this->_cacheDescription($table, $fields); - } - // @codingStandardsIgnoreEnd - - if (isset($model->sequence)) { - $this->_sequenceMap[$table][$model->primaryKey] = $model->sequence; - } - - if ($cols) { - $cols->closeCursor(); - } - return $fields; - } - -/** - * Returns the ID generated from the previous INSERT operation. - * - * @param string $source Name of the database table - * @param string $field Name of the ID database field. Defaults to "id" - * @return int - */ - public function lastInsertId($source = null, $field = 'id') { - $seq = $this->getSequence($source, $field); - return $this->_connection->lastInsertId($seq); - } - -/** - * Gets the associated sequence for the given table/field - * - * @param string|Model $table Either a full table name (with prefix) as a string, or a model object - * @param string $field Name of the ID database field. Defaults to "id" - * @return string The associated sequence name from the sequence map, defaults to "{$table}_{$field}_seq" - */ - public function getSequence($table, $field = 'id') { - if (is_object($table)) { - $table = $this->fullTableName($table, false, false); - } - if (!isset($this->_sequenceMap[$table])) { - $this->describe($table); - } - if (isset($this->_sequenceMap[$table][$field])) { - return $this->_sequenceMap[$table][$field]; - } - return "{$table}_{$field}_seq"; - } - -/** - * Reset a sequence based on the MAX() value of $column. Useful - * for resetting sequences after using insertMulti(). - * - * @param string $table The name of the table to update. - * @param string $column The column to use when resetting the sequence value, - * the sequence name will be fetched using Postgres::getSequence(); - * @return bool success. - */ - public function resetSequence($table, $column) { - $tableName = $this->fullTableName($table, false, false); - $fullTable = $this->fullTableName($table); - - $sequence = $this->value($this->getSequence($tableName, $column)); - $column = $this->name($column); - $this->execute("SELECT setval($sequence, (SELECT MAX($column) FROM $fullTable))"); - return true; - } - -/** - * Deletes all the records in a table and drops all associated auto-increment sequences - * - * @param string|Model $table A string or model class representing the table to be truncated - * @param bool $reset true for resetting the sequence, false to leave it as is. - * and if 1, sequences are not modified - * @return bool SQL TRUNCATE TABLE statement, false if not applicable. - */ - public function truncate($table, $reset = false) { - $table = $this->fullTableName($table, false, false); - if (!isset($this->_sequenceMap[$table])) { - $cache = $this->cacheSources; - $this->cacheSources = false; - $this->describe($table); - $this->cacheSources = $cache; - } - if ($this->execute('DELETE FROM ' . $this->fullTableName($table))) { - if (isset($this->_sequenceMap[$table]) && $reset != true) { - foreach ($this->_sequenceMap[$table] as $sequence) { - $quoted = $this->name($sequence); - $this->_execute("ALTER SEQUENCE {$quoted} RESTART WITH 1"); - } - } - return true; - } - return false; - } - -/** - * Prepares field names to be quoted by parent - * - * @param string $data The name to format. - * @return string SQL field - */ - public function name($data) { - if (is_string($data)) { - $data = str_replace('"__"', '__', $data); - } - return parent::name($data); - } - -/** - * Generates the fields list of an SQL query. - * - * @param Model $model The model to get fields for. - * @param string $alias Alias table name. - * @param mixed $fields The list of fields to get. - * @param bool $quote Whether or not to quote identifiers. - * @return array - */ - public function fields(Model $model, $alias = null, $fields = array(), $quote = true) { - if (empty($alias)) { - $alias = $model->alias; - } - $fields = parent::fields($model, $alias, $fields, false); - - if (!$quote) { - return $fields; - } - $count = count($fields); - - if ($count >= 1 && !preg_match('/^\s*COUNT\(\*/', $fields[0])) { - $result = array(); - for ($i = 0; $i < $count; $i++) { - if (!preg_match('/^.+\\(.*\\)/', $fields[$i]) && !preg_match('/\s+AS\s+/', $fields[$i])) { - if (substr($fields[$i], -1) === '*') { - if (strpos($fields[$i], '.') !== false && $fields[$i] != $alias . '.*') { - $build = explode('.', $fields[$i]); - $AssociatedModel = $model->{$build[0]}; - } else { - $AssociatedModel = $model; - } - - $_fields = $this->fields($AssociatedModel, $AssociatedModel->alias, array_keys($AssociatedModel->schema())); - $result = array_merge($result, $_fields); - continue; - } - - $prepend = ''; - if (strpos($fields[$i], 'DISTINCT') !== false) { - $prepend = 'DISTINCT '; - $fields[$i] = trim(str_replace('DISTINCT', '', $fields[$i])); - } - - if (strrpos($fields[$i], '.') === false) { - $fields[$i] = $prepend . $this->name($alias) . '.' . $this->name($fields[$i]) . ' AS ' . $this->name($alias . '__' . $fields[$i]); - } else { - $build = explode('.', $fields[$i]); - $fields[$i] = $prepend . $this->name($build[0]) . '.' . $this->name($build[1]) . ' AS ' . $this->name($build[0] . '__' . $build[1]); - } - } else { - $fields[$i] = preg_replace_callback('/\(([\s\.\w]+)\)/', array(&$this, '_quoteFunctionField'), $fields[$i]); - } - $result[] = $fields[$i]; - } - return $result; - } - return $fields; - } - -/** - * Auxiliary function to quote matched `(Model.fields)` from a preg_replace_callback call - * Quotes the fields in a function call. - * - * @param string $match matched string - * @return string quoted string - */ - protected function _quoteFunctionField($match) { - $prepend = ''; - if (strpos($match[1], 'DISTINCT') !== false) { - $prepend = 'DISTINCT '; - $match[1] = trim(str_replace('DISTINCT', '', $match[1])); - } - $constant = preg_match('/^\d+|NULL|FALSE|TRUE$/i', $match[1]); - - if (!$constant && strpos($match[1], '.') === false) { - $match[1] = $this->name($match[1]); - } elseif (!$constant) { - $parts = explode('.', $match[1]); - if (!Hash::numeric($parts)) { - $match[1] = $this->name($match[1]); - } - } - return '(' . $prepend . $match[1] . ')'; - } - -/** - * Returns an array of the indexes in given datasource name. - * - * @param string $model Name of model to inspect - * @return array Fields in table. Keys are column and unique - */ - public function index($model) { - $index = array(); - $table = $this->fullTableName($model, false, false); - if ($table) { - $indexes = $this->query("SELECT c2.relname, i.indisprimary, i.indisunique, i.indisclustered, i.indisvalid, pg_catalog.pg_get_indexdef(i.indexrelid, 0, true) as statement, c2.reltablespace + [$table, $this->config['schema'], $this->config['database']] + ); + + // @codingStandardsIgnoreStart + // Postgres columns don't match the coding standards. + foreach ($cols as $c) { + $type = $c->type; + if (!empty($c->oct_length) && $c->char_length === null) { + if ($c->type === 'character varying') { + $length = null; + $type = 'text'; + } else if ($c->type === 'uuid') { + $type = 'uuid'; + $length = 36; + } else { + $length = (int)$c->oct_length; + } + } else if (!empty($c->char_length)) { + $length = (int)$c->char_length; + } else { + $length = $this->length($c->type); + } + if (empty($length)) { + $length = null; + } + $fields[$c->name] = [ + 'type' => $this->column($type), + 'null' => ($c->null === 'NO' ? false : true), + 'default' => preg_replace( + "/^'(.*)'$/", + "$1", + preg_replace('/::[\w\s]+/', '', $c->default) + ), + 'length' => $length, + ]; + + // Serial columns are primary integer keys + if ($c->has_serial) { + $fields[$c->name]['key'] = 'primary'; + $fields[$c->name]['length'] = 11; + $hasPrimary = true; + } + if ($hasPrimary === false && + $model instanceof Model && + $c->name === $model->primaryKey + ) { + $fields[$c->name]['key'] = 'primary'; + if ( + $fields[$c->name]['type'] !== 'string' && + $fields[$c->name]['type'] !== 'uuid' + ) { + $fields[$c->name]['length'] = 11; + } + } + if ( + $fields[$c->name]['default'] === 'NULL' || + $c->default === null || + preg_match('/nextval\([\'"]?([\w.]+)/', $c->default, $seq) + ) { + $fields[$c->name]['default'] = null; + if (!empty($seq) && isset($seq[1])) { + if (strpos($seq[1], '.') === false) { + $sequenceName = $c->schema . '.' . $seq[1]; + } else { + $sequenceName = $seq[1]; + } + $this->_sequenceMap[$table][$c->name] = $sequenceName; + } + } + if ($fields[$c->name]['type'] === 'timestamp' && $fields[$c->name]['default'] === '') { + $fields[$c->name]['default'] = null; + } + if ($fields[$c->name]['type'] === 'boolean' && !empty($fields[$c->name]['default'])) { + $fields[$c->name]['default'] = constant($fields[$c->name]['default']); + } + } + $this->_cacheDescription($table, $fields); + } + // @codingStandardsIgnoreEnd + + if (isset($model->sequence)) { + $this->_sequenceMap[$table][$model->primaryKey] = $model->sequence; + } + + if ($cols) { + $cols->closeCursor(); + } + return $fields; + } + + /** + * Gets the length of a database-native column description, or null if no length + * + * @param string $real Real database-layer column type (i.e. "varchar(255)") + * @return int An integer representing the length of the column + */ + public function length($real) + { + $col = $real; + if (strpos($real, '(') !== false) { + list($col, $limit) = explode('(', $real); + } + if ($col === 'uuid') { + return 36; + } + return parent::length($real); + } + + /** + * Converts database-layer column types to basic types + * + * @param string $real Real database-layer column type (i.e. "varchar(255)") + * @return string Abstract column type (i.e. "string") + */ + public function column($real) + { + if (is_array($real)) { + $col = $real['name']; + if (isset($real['limit'])) { + $col .= '(' . $real['limit'] . ')'; + } + return $col; + } + + $col = str_replace(')', '', $real); + + if (strpos($col, '(') !== false) { + list($col, $limit) = explode('(', $col); + } + + $floats = [ + 'float', 'float4', 'float8', 'double', 'double precision', 'real' + ]; + + switch (true) { + case (in_array($col, ['date', 'time', 'inet', 'boolean'])): + return $col; + case (strpos($col, 'timestamp') !== false): + return 'datetime'; + case (strpos($col, 'time') === 0): + return 'time'; + case ($col === 'bigint'): + return 'biginteger'; + case ($col === 'smallint'): + return 'smallinteger'; + case (strpos($col, 'int') !== false && $col !== 'interval'): + return 'integer'; + case (strpos($col, 'char') !== false): + return 'string'; + case (strpos($col, 'uuid') !== false): + return 'uuid'; + case (strpos($col, 'text') !== false): + return 'text'; + case (strpos($col, 'bytea') !== false): + return 'binary'; + case ($col === 'decimal' || $col === 'numeric'): + return 'decimal'; + case (in_array($col, $floats)): + return 'float'; + default: + return 'text'; + } + } + + /** + * Reset a sequence based on the MAX() value of $column. Useful + * for resetting sequences after using insertMulti(). + * + * @param string $table The name of the table to update. + * @param string $column The column to use when resetting the sequence value, + * the sequence name will be fetched using Postgres::getSequence(); + * @return bool success. + */ + public function resetSequence($table, $column) + { + $tableName = $this->fullTableName($table, false, false); + $fullTable = $this->fullTableName($table); + + $sequence = $this->value($this->getSequence($tableName, $column)); + $column = $this->name($column); + $this->execute("SELECT setval($sequence, (SELECT MAX($column) FROM $fullTable))"); + return true; + } + + /** + * Prepares field names to be quoted by parent + * + * @param string $data The name to format. + * @return string SQL field + */ + public function name($data) + { + if (is_string($data)) { + $data = str_replace('"__"', '__', $data); + } + return parent::name($data); + } + + /** + * Deletes all the records in a table and drops all associated auto-increment sequences + * + * @param string|Model $table A string or model class representing the table to be truncated + * @param bool $reset true for resetting the sequence, false to leave it as is. + * and if 1, sequences are not modified + * @return bool SQL TRUNCATE TABLE statement, false if not applicable. + */ + public function truncate($table, $reset = false) + { + $table = $this->fullTableName($table, false, false); + if (!isset($this->_sequenceMap[$table])) { + $cache = $this->cacheSources; + $this->cacheSources = false; + $this->describe($table); + $this->cacheSources = $cache; + } + if ($this->execute('DELETE FROM ' . $this->fullTableName($table))) { + if (isset($this->_sequenceMap[$table]) && $reset != true) { + foreach ($this->_sequenceMap[$table] as $sequence) { + $quoted = $this->name($sequence); + $this->_execute("ALTER SEQUENCE {$quoted} RESTART WITH 1"); + } + } + return true; + } + return false; + } + + /** + * Generates the fields list of an SQL query. + * + * @param Model $model The model to get fields for. + * @param string $alias Alias table name. + * @param mixed $fields The list of fields to get. + * @param bool $quote Whether or not to quote identifiers. + * @return array + */ + public function fields(Model $model, $alias = null, $fields = [], $quote = true) + { + if (empty($alias)) { + $alias = $model->alias; + } + $fields = parent::fields($model, $alias, $fields, false); + + if (!$quote) { + return $fields; + } + $count = count($fields); + + if ($count >= 1 && !preg_match('/^\s*COUNT\(\*/', $fields[0])) { + $result = []; + for ($i = 0; $i < $count; $i++) { + if (!preg_match('/^.+\\(.*\\)/', $fields[$i]) && !preg_match('/\s+AS\s+/', $fields[$i])) { + if (substr($fields[$i], -1) === '*') { + if (strpos($fields[$i], '.') !== false && $fields[$i] != $alias . '.*') { + $build = explode('.', $fields[$i]); + $AssociatedModel = $model->{$build[0]}; + } else { + $AssociatedModel = $model; + } + + $_fields = $this->fields($AssociatedModel, $AssociatedModel->alias, array_keys($AssociatedModel->schema())); + $result = array_merge($result, $_fields); + continue; + } + + $prepend = ''; + if (strpos($fields[$i], 'DISTINCT') !== false) { + $prepend = 'DISTINCT '; + $fields[$i] = trim(str_replace('DISTINCT', '', $fields[$i])); + } + + if (strrpos($fields[$i], '.') === false) { + $fields[$i] = $prepend . $this->name($alias) . '.' . $this->name($fields[$i]) . ' AS ' . $this->name($alias . '__' . $fields[$i]); + } else { + $build = explode('.', $fields[$i]); + $fields[$i] = $prepend . $this->name($build[0]) . '.' . $this->name($build[1]) . ' AS ' . $this->name($build[0] . '__' . $build[1]); + } + } else { + $fields[$i] = preg_replace_callback('/\(([\s\.\w]+)\)/', [&$this, '_quoteFunctionField'], $fields[$i]); + } + $result[] = $fields[$i]; + } + return $result; + } + return $fields; + } + + /** + * Returns an array of the indexes in given datasource name. + * + * @param string $model Name of model to inspect + * @return array Fields in table. Keys are column and unique + */ + public function index($model) + { + $index = []; + $table = $this->fullTableName($model, false, false); + if ($table) { + $indexes = $this->query("SELECT c2.relname, i.indisprimary, i.indisunique, i.indisclustered, i.indisvalid, pg_catalog.pg_get_indexdef(i.indexrelid, 0, true) as statement, c2.reltablespace FROM pg_catalog.pg_class c, pg_catalog.pg_class c2, pg_catalog.pg_index i WHERE c.oid = ( SELECT c.oid @@ -508,506 +586,452 @@ public function index($model) { ) AND c.oid = i.indrelid AND i.indexrelid = c2.oid ORDER BY i.indisprimary DESC, i.indisunique DESC, c2.relname", false); - foreach ($indexes as $info) { - $key = array_pop($info); - if ($key['indisprimary']) { - $key['relname'] = 'PRIMARY'; - } - preg_match('/\(([^\)]+)\)/', $key['statement'], $indexColumns); - $parsedColumn = $indexColumns[1]; - if (strpos($indexColumns[1], ',') !== false) { - $parsedColumn = explode(', ', $indexColumns[1]); - } - $index[$key['relname']]['unique'] = $key['indisunique']; - $index[$key['relname']]['column'] = $parsedColumn; - } - } - return $index; - } - -/** - * Alter the Schema of a table. - * - * @param array $compare Results of CakeSchema::compare() - * @param string $table name of the table - * @return array - */ - public function alterSchema($compare, $table = null) { - if (!is_array($compare)) { - return false; - } - $out = ''; - $colList = array(); - foreach ($compare as $curTable => $types) { - $indexes = $colList = array(); - if (!$table || $table === $curTable) { - $out .= 'ALTER TABLE ' . $this->fullTableName($curTable) . " \n"; - foreach ($types as $type => $column) { - if (isset($column['indexes'])) { - $indexes[$type] = $column['indexes']; - unset($column['indexes']); - } - switch ($type) { - case 'add': - foreach ($column as $field => $col) { - $col['name'] = $field; - $colList[] = 'ADD COLUMN ' . $this->buildColumn($col); - } - break; - case 'drop': - foreach ($column as $field => $col) { - $col['name'] = $field; - $colList[] = 'DROP COLUMN ' . $this->name($field); - } - break; - case 'change': - $schema = $this->describe($curTable); - foreach ($column as $field => $col) { - if (!isset($col['name'])) { - $col['name'] = $field; - } - $original = $schema[$field]; - $fieldName = $this->name($field); - - $default = isset($col['default']) ? $col['default'] : null; - $nullable = isset($col['null']) ? $col['null'] : null; - $boolToInt = $original['type'] === 'boolean' && $col['type'] === 'integer'; - unset($col['default'], $col['null']); - if ($field !== $col['name']) { - $newName = $this->name($col['name']); - $out .= "\tRENAME {$fieldName} TO {$newName};\n"; - $out .= 'ALTER TABLE ' . $this->fullTableName($curTable) . " \n"; - $fieldName = $newName; - } - - if ($boolToInt) { - $colList[] = 'ALTER COLUMN ' . $fieldName . ' SET DEFAULT NULL'; - $colList[] = 'ALTER COLUMN ' . $fieldName . ' TYPE ' . str_replace(array($fieldName, 'NOT NULL'), '', $this->buildColumn($col)) . ' USING CASE WHEN TRUE THEN 1 ELSE 0 END'; - } else { - if ($original['type'] === 'text' && $col['type'] === 'integer') { - $colList[] = 'ALTER COLUMN ' . $fieldName . ' TYPE ' . str_replace(array($fieldName, 'NOT NULL'), '', $this->buildColumn($col)) . " USING cast({$fieldName} as INTEGER)"; - } else { - $colList[] = 'ALTER COLUMN ' . $fieldName . ' TYPE ' . str_replace(array($fieldName, 'NOT NULL'), '', $this->buildColumn($col)); - } - } - - if (isset($nullable)) { - $nullable = ($nullable) ? 'DROP NOT NULL' : 'SET NOT NULL'; - $colList[] = 'ALTER COLUMN ' . $fieldName . ' ' . $nullable; - } - - if (isset($default)) { - if (!$boolToInt) { - $colList[] = 'ALTER COLUMN ' . $fieldName . ' SET DEFAULT ' . $this->value($default, $col['type']); - } - } else { - $colList[] = 'ALTER COLUMN ' . $fieldName . ' DROP DEFAULT'; - } - - } - break; - } - } - if (isset($indexes['drop']['PRIMARY'])) { - $colList[] = 'DROP CONSTRAINT ' . $curTable . '_pkey'; - } - if (isset($indexes['add']['PRIMARY'])) { - $cols = $indexes['add']['PRIMARY']['column']; - if (is_array($cols)) { - $cols = implode(', ', $cols); - } - $colList[] = 'ADD PRIMARY KEY (' . $cols . ')'; - } - - if (!empty($colList)) { - $out .= "\t" . implode(",\n\t", $colList) . ";\n\n"; - } else { - $out = ''; - } - $out .= implode(";\n\t", $this->_alterIndexes($curTable, $indexes)); - } - } - return $out; - } - -/** - * Generate PostgreSQL index alteration statements for a table. - * - * @param string $table Table to alter indexes for - * @param array $indexes Indexes to add and drop - * @return array Index alteration statements - */ - protected function _alterIndexes($table, $indexes) { - $alter = array(); - if (isset($indexes['drop'])) { - foreach ($indexes['drop'] as $name => $value) { - $out = 'DROP '; - if ($name === 'PRIMARY') { - continue; - } else { - $out .= 'INDEX ' . $name; - } - $alter[] = $out; - } - } - if (isset($indexes['add'])) { - foreach ($indexes['add'] as $name => $value) { - $out = 'CREATE '; - if ($name === 'PRIMARY') { - continue; - } else { - if (!empty($value['unique'])) { - $out .= 'UNIQUE '; - } - $out .= 'INDEX '; - } - if (is_array($value['column'])) { - $out .= $name . ' ON ' . $table . ' (' . implode(', ', array_map(array(&$this, 'name'), $value['column'])) . ')'; - } else { - $out .= $name . ' ON ' . $table . ' (' . $this->name($value['column']) . ')'; - } - $alter[] = $out; - } - } - return $alter; - } - -/** - * Returns a limit statement in the correct format for the particular database. - * - * @param int $limit Limit of results returned - * @param int $offset Offset from which to start results - * @return string SQL limit/offset statement - */ - public function limit($limit, $offset = null) { - if ($limit) { - $rt = sprintf(' LIMIT %u', $limit); - if ($offset) { - $rt .= sprintf(' OFFSET %u', $offset); - } - return $rt; - } - return null; - } - -/** - * Converts database-layer column types to basic types - * - * @param string $real Real database-layer column type (i.e. "varchar(255)") - * @return string Abstract column type (i.e. "string") - */ - public function column($real) { - if (is_array($real)) { - $col = $real['name']; - if (isset($real['limit'])) { - $col .= '(' . $real['limit'] . ')'; - } - return $col; - } - - $col = str_replace(')', '', $real); - - if (strpos($col, '(') !== false) { - list($col, $limit) = explode('(', $col); - } - - $floats = array( - 'float', 'float4', 'float8', 'double', 'double precision', 'real' - ); - - switch (true) { - case (in_array($col, array('date', 'time', 'inet', 'boolean'))): - return $col; - case (strpos($col, 'timestamp') !== false): - return 'datetime'; - case (strpos($col, 'time') === 0): - return 'time'; - case ($col === 'bigint'): - return 'biginteger'; - case ($col === 'smallint'): - return 'smallinteger'; - case (strpos($col, 'int') !== false && $col !== 'interval'): - return 'integer'; - case (strpos($col, 'char') !== false): - return 'string'; - case (strpos($col, 'uuid') !== false): - return 'uuid'; - case (strpos($col, 'text') !== false): - return 'text'; - case (strpos($col, 'bytea') !== false): - return 'binary'; - case ($col === 'decimal' || $col === 'numeric'): - return 'decimal'; - case (in_array($col, $floats)): - return 'float'; - default: - return 'text'; - } - } - -/** - * Gets the length of a database-native column description, or null if no length - * - * @param string $real Real database-layer column type (i.e. "varchar(255)") - * @return int An integer representing the length of the column - */ - public function length($real) { - $col = $real; - if (strpos($real, '(') !== false) { - list($col, $limit) = explode('(', $real); - } - if ($col === 'uuid') { - return 36; - } - return parent::length($real); - } - -/** - * resultSet method - * - * @param PDOStatement $results The results - * @return void - */ - public function resultSet($results) { - $this->map = array(); - $numFields = $results->columnCount(); - $index = 0; - $j = 0; - - while ($j < $numFields) { - $column = $results->getColumnMeta($j); - if (strpos($column['name'], '__')) { - list($table, $name) = explode('__', $column['name']); - $this->map[$index++] = array($table, $name, $column['native_type']); - } else { - $this->map[$index++] = array(0, $column['name'], $column['native_type']); - } - $j++; - } - } - -/** - * Fetches the next row from the current result set - * - * @return array - */ - public function fetchResult() { - if ($row = $this->_result->fetch(PDO::FETCH_NUM)) { - $resultRow = array(); - - foreach ($this->map as $index => $meta) { - list($table, $column, $type) = $meta; - - switch ($type) { - case 'bool': - $resultRow[$table][$column] = $row[$index] === null ? null : $this->boolean($row[$index]); - break; - case 'binary': - case 'bytea': - $resultRow[$table][$column] = $row[$index] === null ? null : stream_get_contents($row[$index]); - break; - default: - $resultRow[$table][$column] = $row[$index]; - } - } - return $resultRow; - } - $this->_result->closeCursor(); - return false; - } - -/** - * Translates between PHP boolean values and PostgreSQL boolean values - * - * @param mixed $data Value to be translated - * @param bool $quote true to quote a boolean to be used in a query, false to return the boolean value - * @return bool Converted boolean value - */ - public function boolean($data, $quote = false) { - switch (true) { - case ($data === true || $data === false): - $result = $data; - break; - case ($data === 't' || $data === 'f'): - $result = ($data === 't'); - break; - case ($data === 'true' || $data === 'false'): - $result = ($data === 'true'); - break; - case ($data === 'TRUE' || $data === 'FALSE'): - $result = ($data === 'TRUE'); - break; - default: - $result = (bool)$data; - } - - if ($quote) { - return ($result) ? 'TRUE' : 'FALSE'; - } - return (bool)$result; - } - -/** - * Sets the database encoding - * - * @param mixed $enc Database encoding - * @return bool True on success, false on failure - */ - public function setEncoding($enc) { - return $this->_execute('SET NAMES ' . $this->value($enc)) !== false; - } - -/** - * Gets the database encoding - * - * @return string The database encoding - */ - public function getEncoding() { - $result = $this->_execute('SHOW client_encoding')->fetch(); - if ($result === false) { - return false; - } - return (isset($result['client_encoding'])) ? $result['client_encoding'] : false; - } - -/** - * Generate a Postgres-native column schema string - * - * @param array $column An array structured like the following: - * array('name'=>'value', 'type'=>'value'[, options]), - * where options can be 'default', 'length', or 'key'. - * @return string - */ - public function buildColumn($column) { - $col = $this->columns[$column['type']]; - if (!isset($col['length']) && !isset($col['limit'])) { - unset($column['length']); - } - $out = parent::buildColumn($column); - - $out = preg_replace( - '/integer\([0-9]+\)/', - 'integer', - $out - ); - $out = preg_replace( - '/bigint\([0-9]+\)/', - 'bigint', - $out - ); - - $out = str_replace('integer serial', 'serial', $out); - $out = str_replace('bigint serial', 'bigserial', $out); - if (strpos($out, 'timestamp DEFAULT')) { - if (isset($column['null']) && $column['null']) { - $out = str_replace('DEFAULT NULL', '', $out); - } else { - $out = str_replace('DEFAULT NOT NULL', '', $out); - } - } - if (strpos($out, 'DEFAULT DEFAULT')) { - if (isset($column['null']) && $column['null']) { - $out = str_replace('DEFAULT DEFAULT', 'DEFAULT NULL', $out); - } elseif (in_array($column['type'], array('integer', 'float'))) { - $out = str_replace('DEFAULT DEFAULT', 'DEFAULT 0', $out); - } elseif ($column['type'] === 'boolean') { - $out = str_replace('DEFAULT DEFAULT', 'DEFAULT FALSE', $out); - } - } - return $out; - } - -/** - * Format indexes for create table - * - * @param array $indexes The index to build - * @param string $table The table name. - * @return string - */ - public function buildIndex($indexes, $table = null) { - $join = array(); - if (!is_array($indexes)) { - return array(); - } - foreach ($indexes as $name => $value) { - if ($name === 'PRIMARY') { - $out = 'PRIMARY KEY (' . $this->name($value['column']) . ')'; - } else { - $out = 'CREATE '; - if (!empty($value['unique'])) { - $out .= 'UNIQUE '; - } - if (is_array($value['column'])) { - $value['column'] = implode(', ', array_map(array(&$this, 'name'), $value['column'])); - } else { - $value['column'] = $this->name($value['column']); - } - $out .= "INDEX {$name} ON {$table}({$value['column']});"; - } - $join[] = $out; - } - return $join; - } - -/** - * {@inheritDoc} - */ - public function value($data, $column = null, $null = true) { - $value = parent::value($data, $column, $null); - if ($column === 'uuid' && is_scalar($data) && $data === '') { - return 'NULL'; - } - return $value; - } - -/** - * Overrides DboSource::renderStatement to handle schema generation with Postgres-style indexes - * - * @param string $type The query type. - * @param array $data The array of data to render. - * @return string - */ - public function renderStatement($type, $data) { - switch (strtolower($type)) { - case 'schema': - extract($data); - - foreach ($indexes as $i => $index) { - if (preg_match('/PRIMARY KEY/', $index)) { - unset($indexes[$i]); - $columns[] = $index; - break; - } - } - $join = array('columns' => ",\n\t", 'indexes' => "\n"); - - foreach (array('columns', 'indexes') as $var) { - if (is_array(${$var})) { - ${$var} = implode($join[$var], array_filter(${$var})); - } - } - return "CREATE TABLE {$table} (\n\t{$columns}\n);\n{$indexes}"; - default: - return parent::renderStatement($type, $data); - } - } - -/** - * Gets the schema name - * - * @return string The schema name - */ - public function getSchemaName() { - return $this->config['schema']; - } - -/** - * Check if the server support nested transactions - * - * @return bool - */ - public function nestedTransactionSupported() { - return $this->useNestedTransactions && version_compare($this->getVersion(), '8.0', '>='); - } + foreach ($indexes as $info) { + $key = array_pop($info); + if ($key['indisprimary']) { + $key['relname'] = 'PRIMARY'; + } + preg_match('/\(([^\)]+)\)/', $key['statement'], $indexColumns); + $parsedColumn = $indexColumns[1]; + if (strpos($indexColumns[1], ',') !== false) { + $parsedColumn = explode(', ', $indexColumns[1]); + } + $index[$key['relname']]['unique'] = $key['indisunique']; + $index[$key['relname']]['column'] = $parsedColumn; + } + } + return $index; + } + + /** + * Alter the Schema of a table. + * + * @param array $compare Results of CakeSchema::compare() + * @param string $table name of the table + * @return array + */ + public function alterSchema($compare, $table = null) + { + if (!is_array($compare)) { + return false; + } + $out = ''; + $colList = []; + foreach ($compare as $curTable => $types) { + $indexes = $colList = []; + if (!$table || $table === $curTable) { + $out .= 'ALTER TABLE ' . $this->fullTableName($curTable) . " \n"; + foreach ($types as $type => $column) { + if (isset($column['indexes'])) { + $indexes[$type] = $column['indexes']; + unset($column['indexes']); + } + switch ($type) { + case 'add': + foreach ($column as $field => $col) { + $col['name'] = $field; + $colList[] = 'ADD COLUMN ' . $this->buildColumn($col); + } + break; + case 'drop': + foreach ($column as $field => $col) { + $col['name'] = $field; + $colList[] = 'DROP COLUMN ' . $this->name($field); + } + break; + case 'change': + $schema = $this->describe($curTable); + foreach ($column as $field => $col) { + if (!isset($col['name'])) { + $col['name'] = $field; + } + $original = $schema[$field]; + $fieldName = $this->name($field); + + $default = isset($col['default']) ? $col['default'] : null; + $nullable = isset($col['null']) ? $col['null'] : null; + $boolToInt = $original['type'] === 'boolean' && $col['type'] === 'integer'; + unset($col['default'], $col['null']); + if ($field !== $col['name']) { + $newName = $this->name($col['name']); + $out .= "\tRENAME {$fieldName} TO {$newName};\n"; + $out .= 'ALTER TABLE ' . $this->fullTableName($curTable) . " \n"; + $fieldName = $newName; + } + + if ($boolToInt) { + $colList[] = 'ALTER COLUMN ' . $fieldName . ' SET DEFAULT NULL'; + $colList[] = 'ALTER COLUMN ' . $fieldName . ' TYPE ' . str_replace([$fieldName, 'NOT NULL'], '', $this->buildColumn($col)) . ' USING CASE WHEN TRUE THEN 1 ELSE 0 END'; + } else { + if ($original['type'] === 'text' && $col['type'] === 'integer') { + $colList[] = 'ALTER COLUMN ' . $fieldName . ' TYPE ' . str_replace([$fieldName, 'NOT NULL'], '', $this->buildColumn($col)) . " USING cast({$fieldName} as INTEGER)"; + } else { + $colList[] = 'ALTER COLUMN ' . $fieldName . ' TYPE ' . str_replace([$fieldName, 'NOT NULL'], '', $this->buildColumn($col)); + } + } + + if (isset($nullable)) { + $nullable = ($nullable) ? 'DROP NOT NULL' : 'SET NOT NULL'; + $colList[] = 'ALTER COLUMN ' . $fieldName . ' ' . $nullable; + } + + if (isset($default)) { + if (!$boolToInt) { + $colList[] = 'ALTER COLUMN ' . $fieldName . ' SET DEFAULT ' . $this->value($default, $col['type']); + } + } else { + $colList[] = 'ALTER COLUMN ' . $fieldName . ' DROP DEFAULT'; + } + + } + break; + } + } + if (isset($indexes['drop']['PRIMARY'])) { + $colList[] = 'DROP CONSTRAINT ' . $curTable . '_pkey'; + } + if (isset($indexes['add']['PRIMARY'])) { + $cols = $indexes['add']['PRIMARY']['column']; + if (is_array($cols)) { + $cols = implode(', ', $cols); + } + $colList[] = 'ADD PRIMARY KEY (' . $cols . ')'; + } + + if (!empty($colList)) { + $out .= "\t" . implode(",\n\t", $colList) . ";\n\n"; + } else { + $out = ''; + } + $out .= implode(";\n\t", $this->_alterIndexes($curTable, $indexes)); + } + } + return $out; + } + + /** + * Generate a Postgres-native column schema string + * + * @param array $column An array structured like the following: + * array('name'=>'value', 'type'=>'value'[, options]), + * where options can be 'default', 'length', or 'key'. + * @return string + */ + public function buildColumn($column) + { + $col = $this->columns[$column['type']]; + if (!isset($col['length']) && !isset($col['limit'])) { + unset($column['length']); + } + $out = parent::buildColumn($column); + + $out = preg_replace( + '/integer\([0-9]+\)/', + 'integer', + $out + ); + $out = preg_replace( + '/bigint\([0-9]+\)/', + 'bigint', + $out + ); + + $out = str_replace('integer serial', 'serial', $out); + $out = str_replace('bigint serial', 'bigserial', $out); + if (strpos($out, 'timestamp DEFAULT')) { + if (isset($column['null']) && $column['null']) { + $out = str_replace('DEFAULT NULL', '', $out); + } else { + $out = str_replace('DEFAULT NOT NULL', '', $out); + } + } + if (strpos($out, 'DEFAULT DEFAULT')) { + if (isset($column['null']) && $column['null']) { + $out = str_replace('DEFAULT DEFAULT', 'DEFAULT NULL', $out); + } else if (in_array($column['type'], ['integer', 'float'])) { + $out = str_replace('DEFAULT DEFAULT', 'DEFAULT 0', $out); + } else if ($column['type'] === 'boolean') { + $out = str_replace('DEFAULT DEFAULT', 'DEFAULT FALSE', $out); + } + } + return $out; + } + + /** + * Generate PostgreSQL index alteration statements for a table. + * + * @param string $table Table to alter indexes for + * @param array $indexes Indexes to add and drop + * @return array Index alteration statements + */ + protected function _alterIndexes($table, $indexes) + { + $alter = []; + if (isset($indexes['drop'])) { + foreach ($indexes['drop'] as $name => $value) { + $out = 'DROP '; + if ($name === 'PRIMARY') { + continue; + } else { + $out .= 'INDEX ' . $name; + } + $alter[] = $out; + } + } + if (isset($indexes['add'])) { + foreach ($indexes['add'] as $name => $value) { + $out = 'CREATE '; + if ($name === 'PRIMARY') { + continue; + } else { + if (!empty($value['unique'])) { + $out .= 'UNIQUE '; + } + $out .= 'INDEX '; + } + if (is_array($value['column'])) { + $out .= $name . ' ON ' . $table . ' (' . implode(', ', array_map([&$this, 'name'], $value['column'])) . ')'; + } else { + $out .= $name . ' ON ' . $table . ' (' . $this->name($value['column']) . ')'; + } + $alter[] = $out; + } + } + return $alter; + } + + /** + * Returns a limit statement in the correct format for the particular database. + * + * @param int $limit Limit of results returned + * @param int $offset Offset from which to start results + * @return string SQL limit/offset statement + */ + public function limit($limit, $offset = null) + { + if ($limit) { + $rt = sprintf(' LIMIT %u', $limit); + if ($offset) { + $rt .= sprintf(' OFFSET %u', $offset); + } + return $rt; + } + return null; + } + + /** + * resultSet method + * + * @param PDOStatement $results The results + * @return void + */ + public function resultSet($results) + { + $this->map = []; + $numFields = $results->columnCount(); + $index = 0; + $j = 0; + + while ($j < $numFields) { + $column = $results->getColumnMeta($j); + if (strpos($column['name'], '__')) { + list($table, $name) = explode('__', $column['name']); + $this->map[$index++] = [$table, $name, $column['native_type']]; + } else { + $this->map[$index++] = [0, $column['name'], $column['native_type']]; + } + $j++; + } + } + + /** + * Fetches the next row from the current result set + * + * @return array + */ + public function fetchResult() + { + if ($row = $this->_result->fetch(PDO::FETCH_NUM)) { + $resultRow = []; + + foreach ($this->map as $index => $meta) { + list($table, $column, $type) = $meta; + + switch ($type) { + case 'bool': + $resultRow[$table][$column] = $row[$index] === null ? null : $this->boolean($row[$index]); + break; + case 'binary': + case 'bytea': + $resultRow[$table][$column] = $row[$index] === null ? null : stream_get_contents($row[$index]); + break; + default: + $resultRow[$table][$column] = $row[$index]; + } + } + return $resultRow; + } + $this->_result->closeCursor(); + return false; + } + + /** + * Translates between PHP boolean values and PostgreSQL boolean values + * + * @param mixed $data Value to be translated + * @param bool $quote true to quote a boolean to be used in a query, false to return the boolean value + * @return bool Converted boolean value + */ + public function boolean($data, $quote = false) + { + switch (true) { + case ($data === true || $data === false): + $result = $data; + break; + case ($data === 't' || $data === 'f'): + $result = ($data === 't'); + break; + case ($data === 'true' || $data === 'false'): + $result = ($data === 'true'); + break; + case ($data === 'TRUE' || $data === 'FALSE'): + $result = ($data === 'TRUE'); + break; + default: + $result = (bool)$data; + } + + if ($quote) { + return ($result) ? 'TRUE' : 'FALSE'; + } + return (bool)$result; + } + + /** + * Gets the database encoding + * + * @return string The database encoding + */ + public function getEncoding() + { + $result = $this->_execute('SHOW client_encoding')->fetch(); + if ($result === false) { + return false; + } + return (isset($result['client_encoding'])) ? $result['client_encoding'] : false; + } + + /** + * Format indexes for create table + * + * @param array $indexes The index to build + * @param string $table The table name. + * @return string + */ + public function buildIndex($indexes, $table = null) + { + $join = []; + if (!is_array($indexes)) { + return []; + } + foreach ($indexes as $name => $value) { + if ($name === 'PRIMARY') { + $out = 'PRIMARY KEY (' . $this->name($value['column']) . ')'; + } else { + $out = 'CREATE '; + if (!empty($value['unique'])) { + $out .= 'UNIQUE '; + } + if (is_array($value['column'])) { + $value['column'] = implode(', ', array_map([&$this, 'name'], $value['column'])); + } else { + $value['column'] = $this->name($value['column']); + } + $out .= "INDEX {$name} ON {$table}({$value['column']});"; + } + $join[] = $out; + } + return $join; + } + + /** + * Overrides DboSource::renderStatement to handle schema generation with Postgres-style indexes + * + * @param string $type The query type. + * @param array $data The array of data to render. + * @return string + */ + public function renderStatement($type, $data) + { + switch (strtolower($type)) { + case 'schema': + extract($data); + + foreach ($indexes as $i => $index) { + if (preg_match('/PRIMARY KEY/', $index)) { + unset($indexes[$i]); + $columns[] = $index; + break; + } + } + $join = ['columns' => ",\n\t", 'indexes' => "\n"]; + + foreach (['columns', 'indexes'] as $var) { + if (is_array(${$var})) { + ${$var} = implode($join[$var], array_filter(${$var})); + } + } + return "CREATE TABLE {$table} (\n\t{$columns}\n);\n{$indexes}"; + default: + return parent::renderStatement($type, $data); + } + } + + /** + * Gets the schema name + * + * @return string The schema name + */ + public function getSchemaName() + { + return $this->config['schema']; + } + + /** + * Check if the server support nested transactions + * + * @return bool + */ + public function nestedTransactionSupported() + { + return $this->useNestedTransactions && version_compare($this->getVersion(), '8.0', '>='); + } + + /** + * Auxiliary function to quote matched `(Model.fields)` from a preg_replace_callback call + * Quotes the fields in a function call. + * + * @param string $match matched string + * @return string quoted string + */ + protected function _quoteFunctionField($match) + { + $prepend = ''; + if (strpos($match[1], 'DISTINCT') !== false) { + $prepend = 'DISTINCT '; + $match[1] = trim(str_replace('DISTINCT', '', $match[1])); + } + $constant = preg_match('/^\d+|NULL|FALSE|TRUE$/i', $match[1]); + + if (!$constant && strpos($match[1], '.') === false) { + $match[1] = $this->name($match[1]); + } else if (!$constant) { + $parts = explode('.', $match[1]); + if (!Hash::numeric($parts)) { + $match[1] = $this->name($match[1]); + } + } + return '(' . $prepend . $match[1] . ')'; + } } diff --git a/lib/Cake/Model/Datasource/Database/Sqlite.php b/lib/Cake/Model/Datasource/Database/Sqlite.php index 487868be..8278025c 100755 --- a/lib/Cake/Model/Datasource/Database/Sqlite.php +++ b/lib/Cake/Model/Datasource/Database/Sqlite.php @@ -26,588 +26,607 @@ * * @package Cake.Model.Datasource.Database */ -class Sqlite extends DboSource { - -/** - * Datasource Description - * - * @var string - */ - public $description = "SQLite DBO Driver"; - -/** - * Quote Start - * - * @var string - */ - public $startQuote = '"'; - -/** - * Quote End - * - * @var string - */ - public $endQuote = '"'; - -/** - * Base configuration settings for SQLite3 driver - * - * @var array - */ - protected $_baseConfig = array( - 'persistent' => false, - 'database' => null, - 'flags' => array() - ); - -/** - * SQLite3 column definition - * - * @var array - * @link https://www.sqlite.org/datatype3.html Datatypes In SQLite Version 3 - */ - public $columns = array( - 'primary_key' => array('name' => 'integer primary key autoincrement'), - 'string' => array('name' => 'varchar', 'limit' => '255'), - 'text' => array('name' => 'text'), - 'integer' => array('name' => 'integer', 'limit' => null, 'formatter' => 'intval'), - 'smallinteger' => array('name' => 'smallint', 'limit' => null, 'formatter' => 'intval'), - 'tinyinteger' => array('name' => 'tinyint', 'limit' => null, 'formatter' => 'intval'), - 'biginteger' => array('name' => 'bigint', 'limit' => 20), - 'float' => array('name' => 'float', 'formatter' => 'floatval'), - 'decimal' => array('name' => 'decimal', 'formatter' => 'floatval'), - 'datetime' => array('name' => 'datetime', 'format' => 'Y-m-d H:i:s', 'formatter' => 'date'), - 'timestamp' => array('name' => 'timestamp', 'format' => 'Y-m-d H:i:s', 'formatter' => 'date'), - 'time' => array('name' => 'time', 'format' => 'H:i:s', 'formatter' => 'date'), - 'date' => array('name' => 'date', 'format' => 'Y-m-d', 'formatter' => 'date'), - 'binary' => array('name' => 'blob'), - 'boolean' => array('name' => 'boolean') - ); - -/** - * List of engine specific additional field parameters used on table creating - * - * @var array - */ - public $fieldParameters = array( - 'collate' => array( - 'value' => 'COLLATE', - 'quote' => false, - 'join' => ' ', - 'column' => 'Collate', - 'position' => 'afterDefault', - 'options' => array( - 'BINARY', 'NOCASE', 'RTRIM' - ) - ), - ); - -/** - * Connects to the database using config['database'] as a filename. - * - * @return bool - * @throws MissingConnectionException - */ - public function connect() { - $config = $this->config; - $flags = $config['flags'] + array( - PDO::ATTR_PERSISTENT => $config['persistent'], - PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION - ); - try { - $this->_connection = new PDO('sqlite:' . $config['database'], null, null, $flags); - $this->connected = true; - } catch(PDOException $e) { - throw new MissingConnectionException(array( - 'class' => get_class($this), - 'message' => $e->getMessage() - )); - } - return $this->connected; - } - -/** - * Check whether the SQLite extension is installed/loaded - * - * @return bool - */ - public function enabled() { - return in_array('sqlite', PDO::getAvailableDrivers()); - } - -/** - * Returns an array of tables in the database. If there are no tables, an error is raised and the application exits. - * - * @param mixed $data Unused. - * @return array Array of table names in the database - */ - public function listSources($data = null) { - $cache = parent::listSources(); - if ($cache) { - return $cache; - } - - $result = $this->fetchAll("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name;", false); - - if (!$result || empty($result)) { - return array(); - } - - $tables = array(); - foreach ($result as $table) { - $tables[] = $table[0]['name']; - } - parent::listSources($tables); - return $tables; - } - -/** - * Returns an array of the fields in given table name. - * - * @param Model|string $model Either the model or table name you want described. - * @return array Fields in table. Keys are name and type - */ - public function describe($model) { - $table = $this->fullTableName($model, false, false); - $cache = parent::describe($table); - if ($cache) { - return $cache; - } - $fields = array(); - $result = $this->_execute( - 'PRAGMA table_info(' . $this->value($table, 'string') . ')' - ); - - foreach ($result as $column) { - $default = ($column['dflt_value'] === 'NULL') ? null : trim($column['dflt_value'], "'"); - - $fields[$column['name']] = array( - 'type' => $this->column($column['type']), - 'null' => !$column['notnull'], - 'default' => $default, - 'length' => $this->length($column['type']) - ); - if (in_array($fields[$column['name']]['type'], array('timestamp', 'datetime')) && strtoupper($fields[$column['name']]['default']) === 'CURRENT_TIMESTAMP') { - $fields[$column['name']]['default'] = null; - } - if ($column['pk'] == 1) { - $fields[$column['name']]['key'] = $this->index['PRI']; - $fields[$column['name']]['null'] = false; - if (empty($fields[$column['name']]['length'])) { - $fields[$column['name']]['length'] = 11; - } - } - } - - $result->closeCursor(); - $this->_cacheDescription($table, $fields); - return $fields; - } - -/** - * Generates and executes an SQL UPDATE statement for given model, fields, and values. - * - * @param Model $model The model instance to update. - * @param array $fields The fields to update. - * @param array $values The values to set columns to. - * @param mixed $conditions array of conditions to use. - * @return bool - */ - public function update(Model $model, $fields = array(), $values = null, $conditions = null) { - if (empty($values) && !empty($fields)) { - foreach ($fields as $field => $value) { - if (strpos($field, $model->alias . '.') !== false) { - unset($fields[$field]); - $field = str_replace($model->alias . '.', "", $field); - $field = str_replace($model->alias . '.', "", $field); - $fields[$field] = $value; - } - } - } - return parent::update($model, $fields, $values, $conditions); - } - -/** - * Deletes all the records in a table and resets the count of the auto-incrementing - * primary key, where applicable. - * - * @param string|Model $table A string or model class representing the table to be truncated - * @return bool SQL TRUNCATE TABLE statement, false if not applicable. - */ - public function truncate($table) { - if (in_array('sqlite_sequence', $this->listSources())) { - $this->_execute('DELETE FROM sqlite_sequence where name=' . $this->startQuote . $this->fullTableName($table, false, false) . $this->endQuote); - } - return $this->execute('DELETE FROM ' . $this->fullTableName($table)); - } - -/** - * Converts database-layer column types to basic types - * - * @param string $real Real database-layer column type (i.e. "varchar(255)") - * @return string Abstract column type (i.e. "string") - */ - public function column($real) { - if (is_array($real)) { - $col = $real['name']; - if (isset($real['limit'])) { - $col .= '(' . $real['limit'] . ')'; - } - return $col; - } - - $col = strtolower(str_replace(')', '', $real)); - if (strpos($col, '(') !== false) { - list($col) = explode('(', $col); - } - - $standard = array( - 'text', - 'integer', - 'float', - 'boolean', - 'timestamp', - 'date', - 'datetime', - 'time' - ); - if (in_array($col, $standard)) { - return $col; - } - if ($col === 'tinyint') { - return 'tinyinteger'; - } - if ($col === 'smallint') { - return 'smallinteger'; - } - if ($col === 'bigint') { - return 'biginteger'; - } - if (strpos($col, 'char') !== false) { - return 'string'; - } - if (in_array($col, array('blob', 'clob'))) { - return 'binary'; - } - if (strpos($col, 'numeric') !== false || strpos($col, 'decimal') !== false) { - return 'decimal'; - } - return 'text'; - } - -/** - * Generate ResultSet - * - * @param PDOStatement $results The results to modify. - * @return void - */ - public function resultSet($results) { - $this->results = $results; - $this->map = array(); - $numFields = $results->columnCount(); - $index = 0; - $j = 0; - - // PDO::getColumnMeta is experimental and does not work with sqlite3, - // so try to figure it out based on the querystring - $querystring = $results->queryString; - if (stripos($querystring, 'SELECT') === 0 && stripos($querystring, 'FROM') > 0) { - $selectpart = substr($querystring, 7); - $selects = array(); - foreach (CakeText::tokenize($selectpart, ',', '(', ')') as $part) { - $fromPos = stripos($part, ' FROM '); - if ($fromPos !== false) { - $selects[] = trim(substr($part, 0, $fromPos)); - break; - } - $selects[] = $part; - } - } elseif (strpos($querystring, 'PRAGMA table_info') === 0) { - $selects = array('cid', 'name', 'type', 'notnull', 'dflt_value', 'pk'); - } elseif (strpos($querystring, 'PRAGMA index_list') === 0) { - $selects = array('seq', 'name', 'unique'); - } elseif (strpos($querystring, 'PRAGMA index_info') === 0) { - $selects = array('seqno', 'cid', 'name'); - } - while ($j < $numFields) { - if (!isset($selects[$j])) { - $j++; - continue; - } - if (preg_match('/\bAS(?!.*\bAS\b)\s+(.*)/i', $selects[$j], $matches)) { - $columnName = trim($matches[1], '"'); - } else { - $columnName = trim(str_replace('"', '', $selects[$j])); - } - - if (strpos($selects[$j], 'DISTINCT') === 0) { - $columnName = str_ireplace('DISTINCT', '', $columnName); - } - - $metaType = false; - try { - $metaData = (array)$results->getColumnMeta($j); - if (!empty($metaData['sqlite:decl_type'])) { - $metaType = trim($metaData['sqlite:decl_type']); - } - } catch (Exception $e) { - } - - if (strpos($columnName, '.')) { - $parts = explode('.', $columnName); - $this->map[$index++] = array(trim($parts[0]), trim($parts[1]), $metaType); - } else { - $this->map[$index++] = array(0, $columnName, $metaType); - } - $j++; - } - } - -/** - * Fetches the next row from the current result set - * - * @return mixed array with results fetched and mapped to column names or false if there is no results left to fetch - */ - public function fetchResult() { - if ($row = $this->_result->fetch(PDO::FETCH_NUM)) { - $resultRow = array(); - foreach ($this->map as $col => $meta) { - list($table, $column, $type) = $meta; - $resultRow[$table][$column] = $row[$col]; - if ($type === 'boolean' && $row[$col] !== null) { - $resultRow[$table][$column] = $this->boolean($resultRow[$table][$column]); - } - } - return $resultRow; - } - $this->_result->closeCursor(); - return false; - } - -/** - * Returns a limit statement in the correct format for the particular database. - * - * @param int $limit Limit of results returned - * @param int $offset Offset from which to start results - * @return string SQL limit/offset statement - */ - public function limit($limit, $offset = null) { - if ($limit) { - $rt = sprintf(' LIMIT %u', $limit); - if ($offset) { - $rt .= sprintf(' OFFSET %u', $offset); - } - return $rt; - } - return null; - } - -/** - * Generate a database-native column schema string - * - * @param array $column An array structured like the following: array('name'=>'value', 'type'=>'value'[, options]), - * where options can be 'default', 'length', or 'key'. - * @return string - */ - public function buildColumn($column) { - $name = $type = null; - $column += array('null' => true); - extract($column); - - if (empty($name) || empty($type)) { - trigger_error(__d('cake_dev', 'Column name or type not defined in schema'), E_USER_WARNING); - return null; - } - - if (!isset($this->columns[$type])) { - trigger_error(__d('cake_dev', 'Column type %s does not exist', $type), E_USER_WARNING); - return null; - } - - $isPrimary = (isset($column['key']) && $column['key'] === 'primary'); - if ($isPrimary && $type === 'integer') { - return $this->name($name) . ' ' . $this->columns['primary_key']['name']; - } - $out = parent::buildColumn($column); - if ($isPrimary && $type === 'biginteger') { - $replacement = 'PRIMARY KEY'; - if ($column['null'] === false) { - $replacement = 'NOT NULL ' . $replacement; - } - return str_replace($this->columns['primary_key']['name'], $replacement, $out); - } - return $out; - } - -/** - * Sets the database encoding - * - * @param string $enc Database encoding - * @return bool - */ - public function setEncoding($enc) { - if (!in_array($enc, array("UTF-8", "UTF-16", "UTF-16le", "UTF-16be"))) { - return false; - } - return $this->_execute("PRAGMA encoding = \"{$enc}\"") !== false; - } - -/** - * Gets the database encoding - * - * @return string The database encoding - */ - public function getEncoding() { - return $this->fetchRow('PRAGMA encoding'); - } - -/** - * Removes redundant primary key indexes, as they are handled in the column def of the key. - * - * @param array $indexes The indexes to build. - * @param string $table The table name. - * @return string The completed index. - */ - public function buildIndex($indexes, $table = null) { - $join = array(); - - $table = str_replace('"', '', $table); - list($dbname, $table) = explode('.', $table); - $dbname = $this->name($dbname); - - foreach ($indexes as $name => $value) { - - if ($name === 'PRIMARY') { - continue; - } - $out = 'CREATE '; - - if (!empty($value['unique'])) { - $out .= 'UNIQUE '; - } - if (is_array($value['column'])) { - $value['column'] = implode(', ', array_map(array(&$this, 'name'), $value['column'])); - } else { - $value['column'] = $this->name($value['column']); - } - $t = trim($table, '"'); - $indexname = $this->name($t . '_' . $name); - $table = $this->name($table); - $out .= "INDEX {$dbname}.{$indexname} ON {$table}({$value['column']});"; - $join[] = $out; - } - return $join; - } - -/** - * Overrides DboSource::index to handle SQLite index introspection - * Returns an array of the indexes in given table name. - * - * @param string $model Name of model to inspect - * @return array Fields in table. Keys are column and unique - */ - public function index($model) { - $index = array(); - $table = $this->fullTableName($model, false, false); - if ($table) { - $indexes = $this->query('PRAGMA index_list(' . $table . ')'); - - if (is_bool($indexes)) { - return array(); - } - foreach ($indexes as $info) { - $key = array_pop($info); - $keyInfo = $this->query('PRAGMA index_info("' . $key['name'] . '")'); - foreach ($keyInfo as $keyCol) { - if (!isset($index[$key['name']])) { - $col = array(); - if (preg_match('/autoindex/', $key['name'])) { - $key['name'] = 'PRIMARY'; - } - $index[$key['name']]['column'] = $keyCol[0]['name']; - $index[$key['name']]['unique'] = (int)$key['unique'] === 1; - } else { - if (!is_array($index[$key['name']]['column'])) { - $col[] = $index[$key['name']]['column']; - } - $col[] = $keyCol[0]['name']; - $index[$key['name']]['column'] = $col; - } - } - } - } - return $index; - } - -/** - * Overrides DboSource::renderStatement to handle schema generation with SQLite-style indexes - * - * @param string $type The type of statement being rendered. - * @param array $data The data to convert to SQL. - * @return string - */ - public function renderStatement($type, $data) { - switch (strtolower($type)) { - case 'schema': - extract($data); - if (is_array($columns)) { - $columns = "\t" . implode(",\n\t", array_filter($columns)); - } - if (is_array($indexes)) { - $indexes = "\t" . implode("\n\t", array_filter($indexes)); - } - return "CREATE TABLE {$table} (\n{$columns});\n{$indexes}"; - default: - return parent::renderStatement($type, $data); - } - } - -/** - * PDO deals in objects, not resources, so overload accordingly. - * - * @return bool - */ - public function hasResult() { - return is_object($this->_result); - } - -/** - * Generate a "drop table" statement for the given table - * - * @param type $table Name of the table to drop - * @return string Drop table SQL statement - */ - protected function _dropTable($table) { - return 'DROP TABLE IF EXISTS ' . $this->fullTableName($table) . ";"; - } - -/** - * Gets the schema name - * - * @return string The schema name - */ - public function getSchemaName() { - return "main"; // Sqlite Datasource does not support multidb - } - -/** - * Check if the server support nested transactions - * - * @return bool - */ - public function nestedTransactionSupported() { - return $this->useNestedTransactions && version_compare($this->getVersion(), '3.6.8', '>='); - } - -/** - * Returns a locking hint for the given mode. - * - * Sqlite Datasource doesn't support row-level locking. - * - * @param mixed $mode Lock mode - * @return string|null Null - */ - public function getLockingHint($mode) { - return null; - } +class Sqlite extends DboSource +{ + + /** + * Datasource Description + * + * @var string + */ + public $description = "SQLite DBO Driver"; + + /** + * Quote Start + * + * @var string + */ + public $startQuote = '"'; + + /** + * Quote End + * + * @var string + */ + public $endQuote = '"'; + /** + * SQLite3 column definition + * + * @var array + * @link https://www.sqlite.org/datatype3.html Datatypes In SQLite Version 3 + */ + public $columns = [ + 'primary_key' => ['name' => 'integer primary key autoincrement'], + 'string' => ['name' => 'varchar', 'limit' => '255'], + 'text' => ['name' => 'text'], + 'integer' => ['name' => 'integer', 'limit' => null, 'formatter' => 'intval'], + 'smallinteger' => ['name' => 'smallint', 'limit' => null, 'formatter' => 'intval'], + 'tinyinteger' => ['name' => 'tinyint', 'limit' => null, 'formatter' => 'intval'], + 'biginteger' => ['name' => 'bigint', 'limit' => 20], + 'float' => ['name' => 'float', 'formatter' => 'floatval'], + 'decimal' => ['name' => 'decimal', 'formatter' => 'floatval'], + 'datetime' => ['name' => 'datetime', 'format' => 'Y-m-d H:i:s', 'formatter' => 'date'], + 'timestamp' => ['name' => 'timestamp', 'format' => 'Y-m-d H:i:s', 'formatter' => 'date'], + 'time' => ['name' => 'time', 'format' => 'H:i:s', 'formatter' => 'date'], + 'date' => ['name' => 'date', 'format' => 'Y-m-d', 'formatter' => 'date'], + 'binary' => ['name' => 'blob'], + 'boolean' => ['name' => 'boolean'] + ]; + /** + * List of engine specific additional field parameters used on table creating + * + * @var array + */ + public $fieldParameters = [ + 'collate' => [ + 'value' => 'COLLATE', + 'quote' => false, + 'join' => ' ', + 'column' => 'Collate', + 'position' => 'afterDefault', + 'options' => [ + 'BINARY', 'NOCASE', 'RTRIM' + ] + ], + ]; + /** + * Base configuration settings for SQLite3 driver + * + * @var array + */ + protected $_baseConfig = [ + 'persistent' => false, + 'database' => null, + 'flags' => [] + ]; + + /** + * Connects to the database using config['database'] as a filename. + * + * @return bool + * @throws MissingConnectionException + */ + public function connect() + { + $config = $this->config; + $flags = $config['flags'] + [ + PDO::ATTR_PERSISTENT => $config['persistent'], + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION + ]; + try { + $this->_connection = new PDO('sqlite:' . $config['database'], null, null, $flags); + $this->connected = true; + } catch (PDOException $e) { + throw new MissingConnectionException([ + 'class' => get_class($this), + 'message' => $e->getMessage() + ]); + } + return $this->connected; + } + + /** + * Check whether the SQLite extension is installed/loaded + * + * @return bool + */ + public function enabled() + { + return in_array('sqlite', PDO::getAvailableDrivers()); + } + + /** + * Returns an array of the fields in given table name. + * + * @param Model|string $model Either the model or table name you want described. + * @return array Fields in table. Keys are name and type + */ + public function describe($model) + { + $table = $this->fullTableName($model, false, false); + $cache = parent::describe($table); + if ($cache) { + return $cache; + } + $fields = []; + $result = $this->_execute( + 'PRAGMA table_info(' . $this->value($table, 'string') . ')' + ); + + foreach ($result as $column) { + $default = ($column['dflt_value'] === 'NULL') ? null : trim($column['dflt_value'], "'"); + + $fields[$column['name']] = [ + 'type' => $this->column($column['type']), + 'null' => !$column['notnull'], + 'default' => $default, + 'length' => $this->length($column['type']) + ]; + if (in_array($fields[$column['name']]['type'], ['timestamp', 'datetime']) && strtoupper($fields[$column['name']]['default']) === 'CURRENT_TIMESTAMP') { + $fields[$column['name']]['default'] = null; + } + if ($column['pk'] == 1) { + $fields[$column['name']]['key'] = $this->index['PRI']; + $fields[$column['name']]['null'] = false; + if (empty($fields[$column['name']]['length'])) { + $fields[$column['name']]['length'] = 11; + } + } + } + + $result->closeCursor(); + $this->_cacheDescription($table, $fields); + return $fields; + } + + /** + * Converts database-layer column types to basic types + * + * @param string $real Real database-layer column type (i.e. "varchar(255)") + * @return string Abstract column type (i.e. "string") + */ + public function column($real) + { + if (is_array($real)) { + $col = $real['name']; + if (isset($real['limit'])) { + $col .= '(' . $real['limit'] . ')'; + } + return $col; + } + + $col = strtolower(str_replace(')', '', $real)); + if (strpos($col, '(') !== false) { + list($col) = explode('(', $col); + } + + $standard = [ + 'text', + 'integer', + 'float', + 'boolean', + 'timestamp', + 'date', + 'datetime', + 'time' + ]; + if (in_array($col, $standard)) { + return $col; + } + if ($col === 'tinyint') { + return 'tinyinteger'; + } + if ($col === 'smallint') { + return 'smallinteger'; + } + if ($col === 'bigint') { + return 'biginteger'; + } + if (strpos($col, 'char') !== false) { + return 'string'; + } + if (in_array($col, ['blob', 'clob'])) { + return 'binary'; + } + if (strpos($col, 'numeric') !== false || strpos($col, 'decimal') !== false) { + return 'decimal'; + } + return 'text'; + } + + /** + * Generates and executes an SQL UPDATE statement for given model, fields, and values. + * + * @param Model $model The model instance to update. + * @param array $fields The fields to update. + * @param array $values The values to set columns to. + * @param mixed $conditions array of conditions to use. + * @return bool + */ + public function update(Model $model, $fields = [], $values = null, $conditions = null) + { + if (empty($values) && !empty($fields)) { + foreach ($fields as $field => $value) { + if (strpos($field, $model->alias . '.') !== false) { + unset($fields[$field]); + $field = str_replace($model->alias . '.', "", $field); + $field = str_replace($model->alias . '.', "", $field); + $fields[$field] = $value; + } + } + } + return parent::update($model, $fields, $values, $conditions); + } + + /** + * Deletes all the records in a table and resets the count of the auto-incrementing + * primary key, where applicable. + * + * @param string|Model $table A string or model class representing the table to be truncated + * @return bool SQL TRUNCATE TABLE statement, false if not applicable. + */ + public function truncate($table) + { + if (in_array('sqlite_sequence', $this->listSources())) { + $this->_execute('DELETE FROM sqlite_sequence where name=' . $this->startQuote . $this->fullTableName($table, false, false) . $this->endQuote); + } + return $this->execute('DELETE FROM ' . $this->fullTableName($table)); + } + + /** + * Returns an array of tables in the database. If there are no tables, an error is raised and the application exits. + * + * @param mixed $data Unused. + * @return array Array of table names in the database + */ + public function listSources($data = null) + { + $cache = parent::listSources(); + if ($cache) { + return $cache; + } + + $result = $this->fetchAll("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name;", false); + + if (!$result || empty($result)) { + return []; + } + + $tables = []; + foreach ($result as $table) { + $tables[] = $table[0]['name']; + } + parent::listSources($tables); + return $tables; + } + + /** + * Generate ResultSet + * + * @param PDOStatement $results The results to modify. + * @return void + */ + public function resultSet($results) + { + $this->results = $results; + $this->map = []; + $numFields = $results->columnCount(); + $index = 0; + $j = 0; + + // PDO::getColumnMeta is experimental and does not work with sqlite3, + // so try to figure it out based on the querystring + $querystring = $results->queryString; + if (stripos($querystring, 'SELECT') === 0 && stripos($querystring, 'FROM') > 0) { + $selectpart = substr($querystring, 7); + $selects = []; + foreach (CakeText::tokenize($selectpart, ',', '(', ')') as $part) { + $fromPos = stripos($part, ' FROM '); + if ($fromPos !== false) { + $selects[] = trim(substr($part, 0, $fromPos)); + break; + } + $selects[] = $part; + } + } else if (strpos($querystring, 'PRAGMA table_info') === 0) { + $selects = ['cid', 'name', 'type', 'notnull', 'dflt_value', 'pk']; + } else if (strpos($querystring, 'PRAGMA index_list') === 0) { + $selects = ['seq', 'name', 'unique']; + } else if (strpos($querystring, 'PRAGMA index_info') === 0) { + $selects = ['seqno', 'cid', 'name']; + } + while ($j < $numFields) { + if (!isset($selects[$j])) { + $j++; + continue; + } + if (preg_match('/\bAS(?!.*\bAS\b)\s+(.*)/i', $selects[$j], $matches)) { + $columnName = trim($matches[1], '"'); + } else { + $columnName = trim(str_replace('"', '', $selects[$j])); + } + + if (strpos($selects[$j], 'DISTINCT') === 0) { + $columnName = str_ireplace('DISTINCT', '', $columnName); + } + + $metaType = false; + try { + $metaData = (array)$results->getColumnMeta($j); + if (!empty($metaData['sqlite:decl_type'])) { + $metaType = trim($metaData['sqlite:decl_type']); + } + } catch (Exception $e) { + } + + if (strpos($columnName, '.')) { + $parts = explode('.', $columnName); + $this->map[$index++] = [trim($parts[0]), trim($parts[1]), $metaType]; + } else { + $this->map[$index++] = [0, $columnName, $metaType]; + } + $j++; + } + } + + /** + * Fetches the next row from the current result set + * + * @return mixed array with results fetched and mapped to column names or false if there is no results left to fetch + */ + public function fetchResult() + { + if ($row = $this->_result->fetch(PDO::FETCH_NUM)) { + $resultRow = []; + foreach ($this->map as $col => $meta) { + list($table, $column, $type) = $meta; + $resultRow[$table][$column] = $row[$col]; + if ($type === 'boolean' && $row[$col] !== null) { + $resultRow[$table][$column] = $this->boolean($resultRow[$table][$column]); + } + } + return $resultRow; + } + $this->_result->closeCursor(); + return false; + } + + /** + * Returns a limit statement in the correct format for the particular database. + * + * @param int $limit Limit of results returned + * @param int $offset Offset from which to start results + * @return string SQL limit/offset statement + */ + public function limit($limit, $offset = null) + { + if ($limit) { + $rt = sprintf(' LIMIT %u', $limit); + if ($offset) { + $rt .= sprintf(' OFFSET %u', $offset); + } + return $rt; + } + return null; + } + + /** + * Generate a database-native column schema string + * + * @param array $column An array structured like the following: array('name'=>'value', 'type'=>'value'[, options]), + * where options can be 'default', 'length', or 'key'. + * @return string + */ + public function buildColumn($column) + { + $name = $type = null; + $column += ['null' => true]; + extract($column); + + if (empty($name) || empty($type)) { + trigger_error(__d('cake_dev', 'Column name or type not defined in schema'), E_USER_WARNING); + return null; + } + + if (!isset($this->columns[$type])) { + trigger_error(__d('cake_dev', 'Column type %s does not exist', $type), E_USER_WARNING); + return null; + } + + $isPrimary = (isset($column['key']) && $column['key'] === 'primary'); + if ($isPrimary && $type === 'integer') { + return $this->name($name) . ' ' . $this->columns['primary_key']['name']; + } + $out = parent::buildColumn($column); + if ($isPrimary && $type === 'biginteger') { + $replacement = 'PRIMARY KEY'; + if ($column['null'] === false) { + $replacement = 'NOT NULL ' . $replacement; + } + return str_replace($this->columns['primary_key']['name'], $replacement, $out); + } + return $out; + } + + /** + * Sets the database encoding + * + * @param string $enc Database encoding + * @return bool + */ + public function setEncoding($enc) + { + if (!in_array($enc, ["UTF-8", "UTF-16", "UTF-16le", "UTF-16be"])) { + return false; + } + return $this->_execute("PRAGMA encoding = \"{$enc}\"") !== false; + } + + /** + * Gets the database encoding + * + * @return string The database encoding + */ + public function getEncoding() + { + return $this->fetchRow('PRAGMA encoding'); + } + + /** + * Removes redundant primary key indexes, as they are handled in the column def of the key. + * + * @param array $indexes The indexes to build. + * @param string $table The table name. + * @return string The completed index. + */ + public function buildIndex($indexes, $table = null) + { + $join = []; + + $table = str_replace('"', '', $table); + list($dbname, $table) = explode('.', $table); + $dbname = $this->name($dbname); + + foreach ($indexes as $name => $value) { + + if ($name === 'PRIMARY') { + continue; + } + $out = 'CREATE '; + + if (!empty($value['unique'])) { + $out .= 'UNIQUE '; + } + if (is_array($value['column'])) { + $value['column'] = implode(', ', array_map([&$this, 'name'], $value['column'])); + } else { + $value['column'] = $this->name($value['column']); + } + $t = trim($table, '"'); + $indexname = $this->name($t . '_' . $name); + $table = $this->name($table); + $out .= "INDEX {$dbname}.{$indexname} ON {$table}({$value['column']});"; + $join[] = $out; + } + return $join; + } + + /** + * Overrides DboSource::index to handle SQLite index introspection + * Returns an array of the indexes in given table name. + * + * @param string $model Name of model to inspect + * @return array Fields in table. Keys are column and unique + */ + public function index($model) + { + $index = []; + $table = $this->fullTableName($model, false, false); + if ($table) { + $indexes = $this->query('PRAGMA index_list(' . $table . ')'); + + if (is_bool($indexes)) { + return []; + } + foreach ($indexes as $info) { + $key = array_pop($info); + $keyInfo = $this->query('PRAGMA index_info("' . $key['name'] . '")'); + foreach ($keyInfo as $keyCol) { + if (!isset($index[$key['name']])) { + $col = []; + if (preg_match('/autoindex/', $key['name'])) { + $key['name'] = 'PRIMARY'; + } + $index[$key['name']]['column'] = $keyCol[0]['name']; + $index[$key['name']]['unique'] = (int)$key['unique'] === 1; + } else { + if (!is_array($index[$key['name']]['column'])) { + $col[] = $index[$key['name']]['column']; + } + $col[] = $keyCol[0]['name']; + $index[$key['name']]['column'] = $col; + } + } + } + } + return $index; + } + + /** + * Overrides DboSource::renderStatement to handle schema generation with SQLite-style indexes + * + * @param string $type The type of statement being rendered. + * @param array $data The data to convert to SQL. + * @return string + */ + public function renderStatement($type, $data) + { + switch (strtolower($type)) { + case 'schema': + extract($data); + if (is_array($columns)) { + $columns = "\t" . implode(",\n\t", array_filter($columns)); + } + if (is_array($indexes)) { + $indexes = "\t" . implode("\n\t", array_filter($indexes)); + } + return "CREATE TABLE {$table} (\n{$columns});\n{$indexes}"; + default: + return parent::renderStatement($type, $data); + } + } + + /** + * PDO deals in objects, not resources, so overload accordingly. + * + * @return bool + */ + public function hasResult() + { + return is_object($this->_result); + } + + /** + * Gets the schema name + * + * @return string The schema name + */ + public function getSchemaName() + { + return "main"; // Sqlite Datasource does not support multidb + } + + /** + * Check if the server support nested transactions + * + * @return bool + */ + public function nestedTransactionSupported() + { + return $this->useNestedTransactions && version_compare($this->getVersion(), '3.6.8', '>='); + } + + /** + * Returns a locking hint for the given mode. + * + * Sqlite Datasource doesn't support row-level locking. + * + * @param mixed $mode Lock mode + * @return string|null Null + */ + public function getLockingHint($mode) + { + return null; + } + + /** + * Generate a "drop table" statement for the given table + * + * @param type $table Name of the table to drop + * @return string Drop table SQL statement + */ + protected function _dropTable($table) + { + return 'DROP TABLE IF EXISTS ' . $this->fullTableName($table) . ";"; + } } diff --git a/lib/Cake/Model/Datasource/Database/Sqlserver.php b/lib/Cake/Model/Datasource/Database/Sqlserver.php index c2593440..58ff0a56 100755 --- a/lib/Cake/Model/Datasource/Database/Sqlserver.php +++ b/lib/Cake/Model/Datasource/Database/Sqlserver.php @@ -28,201 +28,356 @@ * * @package Cake.Model.Datasource.Database */ -class Sqlserver extends DboSource { - -/** - * Driver description - * - * @var string - */ - public $description = "SQL Server DBO Driver"; - -/** - * Starting quote character for quoted identifiers - * - * @var string - */ - public $startQuote = "["; - -/** - * Ending quote character for quoted identifiers - * - * @var string - */ - public $endQuote = "]"; - -/** - * Creates a map between field aliases and numeric indexes. Workaround for the - * SQL Server driver's 30-character column name limitation. - * - * @var array - */ - protected $_fieldMappings = array(); - -/** - * Storing the last affected value - * - * @var mixed - */ - protected $_lastAffected = false; - -/** - * Base configuration settings for MS SQL driver - * - * @var array - */ - protected $_baseConfig = array( - 'host' => 'localhost\SQLEXPRESS', - 'login' => '', - 'password' => '', - 'database' => 'cake', - 'schema' => '', - 'flags' => array() - ); - -/** - * MS SQL column definition - * - * @var array - * @link https://msdn.microsoft.com/en-us/library/ms187752.aspx SQL Server Data Types - */ - public $columns = array( - 'primary_key' => array('name' => 'IDENTITY (1, 1) NOT NULL'), - 'string' => array('name' => 'nvarchar', 'limit' => '255'), - 'text' => array('name' => 'nvarchar', 'limit' => 'MAX'), - 'integer' => array('name' => 'int', 'formatter' => 'intval'), - 'smallinteger' => array('name' => 'smallint', 'formatter' => 'intval'), - 'tinyinteger' => array('name' => 'tinyint', 'formatter' => 'intval'), - 'biginteger' => array('name' => 'bigint'), - 'numeric' => array('name' => 'decimal', 'formatter' => 'floatval'), - 'decimal' => array('name' => 'decimal', 'formatter' => 'floatval'), - 'float' => array('name' => 'float', 'formatter' => 'floatval'), - 'real' => array('name' => 'float', 'formatter' => 'floatval'), - 'datetime' => array('name' => 'datetime', 'format' => 'Y-m-d H:i:s', 'formatter' => 'date'), - 'timestamp' => array('name' => 'timestamp', 'format' => 'Y-m-d H:i:s', 'formatter' => 'date'), - 'time' => array('name' => 'datetime', 'format' => 'H:i:s', 'formatter' => 'date'), - 'date' => array('name' => 'datetime', 'format' => 'Y-m-d', 'formatter' => 'date'), - 'binary' => array('name' => 'varbinary'), - 'boolean' => array('name' => 'bit') - ); - -/** - * Magic column name used to provide pagination support for SQLServer 2008 - * which lacks proper limit/offset support. - * - * @var string - */ - const ROW_COUNTER = '_cake_page_rownum_'; - -/** - * Connects to the database using options in the given configuration array. - * - * Please note that the PDO::ATTR_PERSISTENT attribute is not supported by - * the SQL Server PHP PDO drivers. As a result you cannot use the - * persistent config option when connecting to a SQL Server (for more - * information see: https://github.com/Microsoft/msphpsql/issues/65). - * - * @return bool True if the database could be connected, else false - * @throws InvalidArgumentException if an unsupported setting is in the database config - * @throws MissingConnectionException - */ - public function connect() { - $config = $this->config; - $this->connected = false; - - if (isset($config['persistent']) && $config['persistent']) { - throw new InvalidArgumentException('Config setting "persistent" cannot be set to true, as the Sqlserver PDO driver does not support PDO::ATTR_PERSISTENT'); - } - - $flags = $config['flags'] + array( - PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION - ); - - if (!empty($config['encoding'])) { - $flags[PDO::SQLSRV_ATTR_ENCODING] = $config['encoding']; - } - - try { - $this->_connection = new PDO( - "sqlsrv:server={$config['host']};Database={$config['database']}", - $config['login'], - $config['password'], - $flags - ); - $this->connected = true; - if (!empty($config['settings'])) { - foreach ($config['settings'] as $key => $value) { - $this->_execute("SET $key $value"); - } - } - } catch (PDOException $e) { - throw new MissingConnectionException(array( - 'class' => get_class($this), - 'message' => $e->getMessage() - )); - } - - return $this->connected; - } - -/** - * Check that PDO SQL Server is installed/loaded - * - * @return bool - */ - public function enabled() { - return in_array('sqlsrv', PDO::getAvailableDrivers()); - } - -/** - * Returns an array of sources (tables) in the database. - * - * @param mixed $data The names - * @return array Array of table names in the database - */ - public function listSources($data = null) { - $cache = parent::listSources(); - if ($cache !== null) { - return $cache; - } - $result = $this->_execute("SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES"); - - if (!$result) { - $result->closeCursor(); - return array(); - } - $tables = array(); - - while ($line = $result->fetch(PDO::FETCH_NUM)) { - $tables[] = $line[0]; - } - - $result->closeCursor(); - parent::listSources($tables); - return $tables; - } - -/** - * Returns an array of the fields in given table name. - * - * @param Model|string $model Model object to describe, or a string table name. - * @return array Fields in table. Keys are name and type - * @throws CakeException - */ - public function describe($model) { - $table = $this->fullTableName($model, false, false); - $fulltable = $this->fullTableName($model, false, true); - - $cache = parent::describe($fulltable); - if ($cache) { - return $cache; - } - - $fields = array(); - $schema = is_object($model) ? $model->schemaName : false; - - $cols = $this->_execute( - "SELECT +class Sqlserver extends DboSource +{ + + /** + * Magic column name used to provide pagination support for SQLServer 2008 + * which lacks proper limit/offset support. + * + * @var string + */ + const ROW_COUNTER = '_cake_page_rownum_'; + /** + * Driver description + * + * @var string + */ + public $description = "SQL Server DBO Driver"; + /** + * Starting quote character for quoted identifiers + * + * @var string + */ + public $startQuote = "["; + /** + * Ending quote character for quoted identifiers + * + * @var string + */ + public $endQuote = "]"; + /** + * MS SQL column definition + * + * @var array + * @link https://msdn.microsoft.com/en-us/library/ms187752.aspx SQL Server Data Types + */ + public $columns = [ + 'primary_key' => ['name' => 'IDENTITY (1, 1) NOT NULL'], + 'string' => ['name' => 'nvarchar', 'limit' => '255'], + 'text' => ['name' => 'nvarchar', 'limit' => 'MAX'], + 'integer' => ['name' => 'int', 'formatter' => 'intval'], + 'smallinteger' => ['name' => 'smallint', 'formatter' => 'intval'], + 'tinyinteger' => ['name' => 'tinyint', 'formatter' => 'intval'], + 'biginteger' => ['name' => 'bigint'], + 'numeric' => ['name' => 'decimal', 'formatter' => 'floatval'], + 'decimal' => ['name' => 'decimal', 'formatter' => 'floatval'], + 'float' => ['name' => 'float', 'formatter' => 'floatval'], + 'real' => ['name' => 'float', 'formatter' => 'floatval'], + 'datetime' => ['name' => 'datetime', 'format' => 'Y-m-d H:i:s', 'formatter' => 'date'], + 'timestamp' => ['name' => 'timestamp', 'format' => 'Y-m-d H:i:s', 'formatter' => 'date'], + 'time' => ['name' => 'datetime', 'format' => 'H:i:s', 'formatter' => 'date'], + 'date' => ['name' => 'datetime', 'format' => 'Y-m-d', 'formatter' => 'date'], + 'binary' => ['name' => 'varbinary'], + 'boolean' => ['name' => 'bit'] + ]; + /** + * Creates a map between field aliases and numeric indexes. Workaround for the + * SQL Server driver's 30-character column name limitation. + * + * @var array + */ + protected $_fieldMappings = []; + /** + * Storing the last affected value + * + * @var mixed + */ + protected $_lastAffected = false; + /** + * Base configuration settings for MS SQL driver + * + * @var array + */ + protected $_baseConfig = [ + 'host' => 'localhost\SQLEXPRESS', + 'login' => '', + 'password' => '', + 'database' => 'cake', + 'schema' => '', + 'flags' => [] + ]; + + /** + * Connects to the database using options in the given configuration array. + * + * Please note that the PDO::ATTR_PERSISTENT attribute is not supported by + * the SQL Server PHP PDO drivers. As a result you cannot use the + * persistent config option when connecting to a SQL Server (for more + * information see: https://github.com/Microsoft/msphpsql/issues/65). + * + * @return bool True if the database could be connected, else false + * @throws InvalidArgumentException if an unsupported setting is in the database config + * @throws MissingConnectionException + */ + public function connect() + { + $config = $this->config; + $this->connected = false; + + if (isset($config['persistent']) && $config['persistent']) { + throw new InvalidArgumentException('Config setting "persistent" cannot be set to true, as the Sqlserver PDO driver does not support PDO::ATTR_PERSISTENT'); + } + + $flags = $config['flags'] + [ + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION + ]; + + if (!empty($config['encoding'])) { + $flags[PDO::SQLSRV_ATTR_ENCODING] = $config['encoding']; + } + + try { + $this->_connection = new PDO( + "sqlsrv:server={$config['host']};Database={$config['database']}", + $config['login'], + $config['password'], + $flags + ); + $this->connected = true; + if (!empty($config['settings'])) { + foreach ($config['settings'] as $key => $value) { + $this->_execute("SET $key $value"); + } + } + } catch (PDOException $e) { + throw new MissingConnectionException([ + 'class' => get_class($this), + 'message' => $e->getMessage() + ]); + } + + return $this->connected; + } + + /** + * Executes given SQL statement. + * + * @param string $sql SQL statement + * @param array $params list of params to be bound to query (supported only in select) + * @param array $prepareOptions Options to be used in the prepare statement + * @return mixed PDOStatement if query executes with no problem, true as the result of a successful, false on error + * query returning no rows, such as a CREATE statement, false otherwise + * @throws PDOException + */ + protected function _execute($sql, $params = [], $prepareOptions = []) + { + $this->_lastAffected = false; + $sql = trim($sql); + if (strncasecmp($sql, 'SELECT', 6) === 0 || preg_match('/^EXEC(?:UTE)?\s/mi', $sql) > 0) { + $prepareOptions += [PDO::ATTR_CURSOR => PDO::CURSOR_SCROLL]; + return parent::_execute($sql, $params, $prepareOptions); + } + try { + $this->_lastAffected = $this->_connection->exec($sql); + if ($this->_lastAffected === false) { + $this->_results = null; + $error = $this->_connection->errorInfo(); + $this->error = $error[2]; + return false; + } + return true; + } catch (PDOException $e) { + if (isset($query->queryString)) { + $e->queryString = $query->queryString; + } else { + $e->queryString = $sql; + } + throw $e; + } + } + + /** + * Check that PDO SQL Server is installed/loaded + * + * @return bool + */ + public function enabled() + { + return in_array('sqlsrv', PDO::getAvailableDrivers()); + } + + /** + * Returns an array of sources (tables) in the database. + * + * @param mixed $data The names + * @return array Array of table names in the database + */ + public function listSources($data = null) + { + $cache = parent::listSources(); + if ($cache !== null) { + return $cache; + } + $result = $this->_execute("SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES"); + + if (!$result) { + $result->closeCursor(); + return []; + } + $tables = []; + + while ($line = $result->fetch(PDO::FETCH_NUM)) { + $tables[] = $line[0]; + } + + $result->closeCursor(); + parent::listSources($tables); + return $tables; + } + + /** + * Generates the fields list of an SQL query. + * + * @param Model $model The model to get fields for. + * @param string $alias Alias table name + * @param array $fields The fields so far. + * @param bool $quote Whether or not to quote identfiers. + * @return array + */ + public function fields(Model $model, $alias = null, $fields = [], $quote = true) + { + if (empty($alias)) { + $alias = $model->alias; + } + $fields = parent::fields($model, $alias, $fields, false); + $count = count($fields); + + if ($count >= 1 && strpos($fields[0], 'COUNT(*)') === false) { + $result = []; + for ($i = 0; $i < $count; $i++) { + $prepend = ''; + + if (strpos($fields[$i], 'DISTINCT') !== false && strpos($fields[$i], 'COUNT') === false) { + $prepend = 'DISTINCT '; + $fields[$i] = trim(str_replace('DISTINCT', '', $fields[$i])); + } + if (strpos($fields[$i], 'COUNT(DISTINCT') !== false) { + $prepend = 'COUNT(DISTINCT '; + $fields[$i] = trim(str_replace('COUNT(DISTINCT', '', $this->_quoteFields($fields[$i]))); + } + + if (!preg_match('/\s+AS\s+/i', $fields[$i])) { + if (substr($fields[$i], -1) === '*') { + if (strpos($fields[$i], '.') !== false && $fields[$i] != $alias . '.*') { + $build = explode('.', $fields[$i]); + $AssociatedModel = $model->{$build[0]}; + } else { + $AssociatedModel = $model; + } + + $_fields = $this->fields($AssociatedModel, $AssociatedModel->alias, array_keys($AssociatedModel->schema())); + $result = array_merge($result, $_fields); + continue; + } + + if (strpos($fields[$i], '.') === false) { + $this->_fieldMappings[$alias . '__' . $fields[$i]] = $alias . '.' . $fields[$i]; + $fieldName = $this->name($alias . '.' . $fields[$i]); + $fieldAlias = $this->name($alias . '__' . $fields[$i]); + } else { + $build = explode('.', $fields[$i]); + $build[0] = trim($build[0], '[]'); + $build[1] = trim($build[1], '[]'); + $name = $build[0] . '.' . $build[1]; + $alias = $build[0] . '__' . $build[1]; + + $this->_fieldMappings[$alias] = $name; + $fieldName = $this->name($name); + $fieldAlias = $this->name($alias); + } + if ($model->getColumnType($fields[$i]) === 'datetime') { + $fieldName = "CONVERT(VARCHAR(20), {$fieldName}, 20)"; + } + $fields[$i] = "{$fieldName} AS {$fieldAlias}"; + } + $result[] = $prepend . $fields[$i]; + } + return $result; + } + return $fields; + } + + /** + * Generates and executes an SQL INSERT statement for given model, fields, and values. + * Removes Identity (primary key) column from update data before returning to parent, if + * value is empty. + * + * @param Model $model The model to insert into. + * @param array $fields The fields to set. + * @param array $values The values to set. + * @return array + */ + public function create(Model $model, $fields = null, $values = null) + { + if (!empty($values)) { + $fields = array_combine($fields, $values); + } + $primaryKey = $this->_getPrimaryKey($model); + + if (array_key_exists($primaryKey, $fields)) { + if (empty($fields[$primaryKey])) { + unset($fields[$primaryKey]); + } else { + $this->_execute('SET IDENTITY_INSERT ' . $this->fullTableName($model) . ' ON'); + } + } + $result = parent::create($model, array_keys($fields), array_values($fields)); + if (array_key_exists($primaryKey, $fields) && !empty($fields[$primaryKey])) { + $this->_execute('SET IDENTITY_INSERT ' . $this->fullTableName($model) . ' OFF'); + } + return $result; + } + + /** + * Makes sure it will return the primary key + * + * @param Model|string $model Model instance of table name + * @return string + */ + protected function _getPrimaryKey($model) + { + $schema = $this->describe($model); + foreach ($schema as $field => $props) { + if (isset($props['key']) && $props['key'] === 'primary') { + return $field; + } + } + return null; + } + + /** + * Returns an array of the fields in given table name. + * + * @param Model|string $model Model object to describe, or a string table name. + * @return array Fields in table. Keys are name and type + * @throws CakeException + */ + public function describe($model) + { + $table = $this->fullTableName($model, false, false); + $fulltable = $this->fullTableName($model, false, true); + + $cache = parent::describe($fulltable); + if ($cache) { + return $cache; + } + + $fields = []; + $schema = is_object($model) ? $model->schemaName : false; + + $cols = $this->_execute( + "SELECT COLUMN_NAME as Field, DATA_TYPE as Type, COL_LENGTH('" . ($schema ? $fulltable : $table) . "', COLUMN_NAME) as Length, @@ -232,624 +387,487 @@ public function describe($model) { NUMERIC_SCALE as Size FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = '" . $table . "'" . ($schema ? " AND TABLE_SCHEMA = '" . $schema . "'" : '') - ); - - if (!$cols) { - throw new CakeException(__d('cake_dev', 'Could not describe table for %s', $table)); - } - - while ($column = $cols->fetch(PDO::FETCH_OBJ)) { - $field = $column->Field; - $fields[$field] = array( - 'type' => $this->column($column), - 'null' => ($column->Null === 'YES' ? true : false), - 'default' => $column->Default, - 'length' => $this->length($column), - 'key' => ($column->Key == '1') ? 'primary' : false - ); - - if ($fields[$field]['default'] === 'null') { - $fields[$field]['default'] = null; - } - if ($fields[$field]['default'] !== null) { - $fields[$field]['default'] = preg_replace( - "/^[(]{1,2}'?([^')]*)?'?[)]{1,2}$/", - "$1", - $fields[$field]['default'] - ); - $this->value($fields[$field]['default'], $fields[$field]['type']); - } - - if ($fields[$field]['key'] !== false && $fields[$field]['type'] === 'integer') { - $fields[$field]['length'] = 11; - } elseif ($fields[$field]['key'] === false) { - unset($fields[$field]['key']); - } - if (in_array($fields[$field]['type'], array('date', 'time', 'datetime', 'timestamp'))) { - $fields[$field]['length'] = null; - } - if ($fields[$field]['type'] === 'float' && !empty($column->Size)) { - $fields[$field]['length'] = $fields[$field]['length'] . ',' . $column->Size; - } - } - $this->_cacheDescription($table, $fields); - $cols->closeCursor(); - return $fields; - } - -/** - * Generates the fields list of an SQL query. - * - * @param Model $model The model to get fields for. - * @param string $alias Alias table name - * @param array $fields The fields so far. - * @param bool $quote Whether or not to quote identfiers. - * @return array - */ - public function fields(Model $model, $alias = null, $fields = array(), $quote = true) { - if (empty($alias)) { - $alias = $model->alias; - } - $fields = parent::fields($model, $alias, $fields, false); - $count = count($fields); - - if ($count >= 1 && strpos($fields[0], 'COUNT(*)') === false) { - $result = array(); - for ($i = 0; $i < $count; $i++) { - $prepend = ''; - - if (strpos($fields[$i], 'DISTINCT') !== false && strpos($fields[$i], 'COUNT') === false) { - $prepend = 'DISTINCT '; - $fields[$i] = trim(str_replace('DISTINCT', '', $fields[$i])); - } - if (strpos($fields[$i], 'COUNT(DISTINCT') !== false) { - $prepend = 'COUNT(DISTINCT '; - $fields[$i] = trim(str_replace('COUNT(DISTINCT', '', $this->_quoteFields($fields[$i]))); - } - - if (!preg_match('/\s+AS\s+/i', $fields[$i])) { - if (substr($fields[$i], -1) === '*') { - if (strpos($fields[$i], '.') !== false && $fields[$i] != $alias . '.*') { - $build = explode('.', $fields[$i]); - $AssociatedModel = $model->{$build[0]}; - } else { - $AssociatedModel = $model; - } - - $_fields = $this->fields($AssociatedModel, $AssociatedModel->alias, array_keys($AssociatedModel->schema())); - $result = array_merge($result, $_fields); - continue; - } - - if (strpos($fields[$i], '.') === false) { - $this->_fieldMappings[$alias . '__' . $fields[$i]] = $alias . '.' . $fields[$i]; - $fieldName = $this->name($alias . '.' . $fields[$i]); - $fieldAlias = $this->name($alias . '__' . $fields[$i]); - } else { - $build = explode('.', $fields[$i]); - $build[0] = trim($build[0], '[]'); - $build[1] = trim($build[1], '[]'); - $name = $build[0] . '.' . $build[1]; - $alias = $build[0] . '__' . $build[1]; - - $this->_fieldMappings[$alias] = $name; - $fieldName = $this->name($name); - $fieldAlias = $this->name($alias); - } - if ($model->getColumnType($fields[$i]) === 'datetime') { - $fieldName = "CONVERT(VARCHAR(20), {$fieldName}, 20)"; - } - $fields[$i] = "{$fieldName} AS {$fieldAlias}"; - } - $result[] = $prepend . $fields[$i]; - } - return $result; - } - return $fields; - } - -/** - * Generates and executes an SQL INSERT statement for given model, fields, and values. - * Removes Identity (primary key) column from update data before returning to parent, if - * value is empty. - * - * @param Model $model The model to insert into. - * @param array $fields The fields to set. - * @param array $values The values to set. - * @return array - */ - public function create(Model $model, $fields = null, $values = null) { - if (!empty($values)) { - $fields = array_combine($fields, $values); - } - $primaryKey = $this->_getPrimaryKey($model); - - if (array_key_exists($primaryKey, $fields)) { - if (empty($fields[$primaryKey])) { - unset($fields[$primaryKey]); - } else { - $this->_execute('SET IDENTITY_INSERT ' . $this->fullTableName($model) . ' ON'); - } - } - $result = parent::create($model, array_keys($fields), array_values($fields)); - if (array_key_exists($primaryKey, $fields) && !empty($fields[$primaryKey])) { - $this->_execute('SET IDENTITY_INSERT ' . $this->fullTableName($model) . ' OFF'); - } - return $result; - } - -/** - * Generates and executes an SQL UPDATE statement for given model, fields, and values. - * Removes Identity (primary key) column from update data before returning to parent. - * - * @param Model $model The model to update. - * @param array $fields The fields to set. - * @param array $values The values to set. - * @param mixed $conditions The conditions to use. - * @return array - */ - public function update(Model $model, $fields = array(), $values = null, $conditions = null) { - if (!empty($values)) { - $fields = array_combine($fields, $values); - } - if (isset($fields[$model->primaryKey])) { - unset($fields[$model->primaryKey]); - } - if (empty($fields)) { - return true; - } - return parent::update($model, array_keys($fields), array_values($fields), $conditions); - } - -/** - * Returns a limit statement in the correct format for the particular database. - * - * @param int $limit Limit of results returned - * @param int $offset Offset from which to start results - * @return string SQL limit/offset statement - */ - public function limit($limit, $offset = null) { - if ($limit) { - $rt = ''; - if (!strpos(strtolower($limit), 'top') || strpos(strtolower($limit), 'top') === 0) { - $rt = ' TOP'; - } - $rt .= sprintf(' %u', $limit); - if ((is_int($offset) || ctype_digit($offset)) && $offset > 0) { - $rt = sprintf(' OFFSET %u ROWS FETCH FIRST %u ROWS ONLY', $offset, $limit); - } - return $rt; - } - return null; - } - -/** - * Converts database-layer column types to basic types - * - * @param mixed $real Either the string value of the fields type. - * or the Result object from Sqlserver::describe() - * @return string Abstract column type (i.e. "string") - */ - public function column($real) { - $limit = null; - $col = $real; - if (is_object($real) && isset($real->Field)) { - $limit = $real->Length; - $col = $real->Type; - } - - if ($col === 'datetime2') { - return 'datetime'; - } - if (in_array($col, array('date', 'time', 'datetime', 'timestamp'))) { - return $col; - } - if ($col === 'bit') { - return 'boolean'; - } - if (strpos($col, 'bigint') !== false) { - return 'biginteger'; - } - if (strpos($col, 'smallint') !== false) { - return 'smallinteger'; - } - if (strpos($col, 'tinyint') !== false) { - return 'tinyinteger'; - } - if (strpos($col, 'int') !== false) { - return 'integer'; - } - if (strpos($col, 'char') !== false && $limit == -1) { - return 'text'; - } - if (strpos($col, 'char') !== false) { - return 'string'; - } - if (strpos($col, 'text') !== false) { - return 'text'; - } - if (strpos($col, 'binary') !== false || $col === 'image') { - return 'binary'; - } - if (in_array($col, array('float', 'real'))) { - return 'float'; - } - if (in_array($col, array('decimal', 'numeric'))) { - return 'decimal'; - } - return 'text'; - } - -/** - * Handle SQLServer specific length properties. - * SQLServer handles text types as nvarchar/varchar with a length of -1. - * - * @param mixed $length Either the length as a string, or a Column descriptor object. - * @return mixed null|integer with length of column. - */ - public function length($length) { - if (is_object($length) && isset($length->Length)) { - if ($length->Length == -1 && strpos($length->Type, 'char') !== false) { - return null; - } - if (in_array($length->Type, array('nchar', 'nvarchar'))) { - return floor($length->Length / 2); - } - if ($length->Type === 'text') { - return null; - } - return $length->Length; - } - return parent::length($length); - } - -/** - * Builds a map of the columns contained in a result - * - * @param PDOStatement $results The result to modify. - * @return void - */ - public function resultSet($results) { - $this->map = array(); - $numFields = $results->columnCount(); - $index = 0; - - while ($numFields-- > 0) { - $column = $results->getColumnMeta($index); - $name = $column['name']; - - if (strpos($name, '__')) { - if (isset($this->_fieldMappings[$name]) && strpos($this->_fieldMappings[$name], '.')) { - $map = explode('.', $this->_fieldMappings[$name]); - } elseif (isset($this->_fieldMappings[$name])) { - $map = array(0, $this->_fieldMappings[$name]); - } else { - $map = array(0, $name); - } - } else { - $map = array(0, $name); - } - $map[] = ($column['sqlsrv:decl_type'] === 'bit') ? 'boolean' : $column['native_type']; - $this->map[$index++] = $map; - } - } - -/** - * Builds final SQL statement - * - * @param string $type Query type - * @param array $data Query data - * @return string - */ - public function renderStatement($type, $data) { - switch (strtolower($type)) { - case 'select': - extract($data); - $fields = trim($fields); - - $having = !empty($having) ? " $having" : ''; - $lock = !empty($lock) ? " $lock" : ''; - - if (strpos($limit, 'TOP') !== false && strpos($fields, 'DISTINCT ') === 0) { - $limit = 'DISTINCT ' . trim($limit); - $fields = substr($fields, 9); - } - - // hack order as SQLServer requires an order if there is a limit. - if ($limit && !$order) { - $order = 'ORDER BY (SELECT NULL)'; - } - - // For older versions use the subquery version of pagination. - if (version_compare($this->getVersion(), '11', '<') && preg_match('/FETCH\sFIRST\s+([0-9]+)/i', $limit, $offset)) { - preg_match('/OFFSET\s*(\d+)\s*.*?(\d+)\s*ROWS/', $limit, $limitOffset); - - $limit = 'TOP ' . (int)$limitOffset[2]; - $page = (int)($limitOffset[1] / $limitOffset[2]); - $offset = (int)($limitOffset[2] * $page); - - $rowCounter = static::ROW_COUNTER; - $sql = "SELECT {$limit} * FROM ( + ); + + if (!$cols) { + throw new CakeException(__d('cake_dev', 'Could not describe table for %s', $table)); + } + + while ($column = $cols->fetch(PDO::FETCH_OBJ)) { + $field = $column->Field; + $fields[$field] = [ + 'type' => $this->column($column), + 'null' => ($column->Null === 'YES' ? true : false), + 'default' => $column->Default, + 'length' => $this->length($column), + 'key' => ($column->Key == '1') ? 'primary' : false + ]; + + if ($fields[$field]['default'] === 'null') { + $fields[$field]['default'] = null; + } + if ($fields[$field]['default'] !== null) { + $fields[$field]['default'] = preg_replace( + "/^[(]{1,2}'?([^')]*)?'?[)]{1,2}$/", + "$1", + $fields[$field]['default'] + ); + $this->value($fields[$field]['default'], $fields[$field]['type']); + } + + if ($fields[$field]['key'] !== false && $fields[$field]['type'] === 'integer') { + $fields[$field]['length'] = 11; + } else if ($fields[$field]['key'] === false) { + unset($fields[$field]['key']); + } + if (in_array($fields[$field]['type'], ['date', 'time', 'datetime', 'timestamp'])) { + $fields[$field]['length'] = null; + } + if ($fields[$field]['type'] === 'float' && !empty($column->Size)) { + $fields[$field]['length'] = $fields[$field]['length'] . ',' . $column->Size; + } + } + $this->_cacheDescription($table, $fields); + $cols->closeCursor(); + return $fields; + } + + /** + * Converts database-layer column types to basic types + * + * @param mixed $real Either the string value of the fields type. + * or the Result object from Sqlserver::describe() + * @return string Abstract column type (i.e. "string") + */ + public function column($real) + { + $limit = null; + $col = $real; + if (is_object($real) && isset($real->Field)) { + $limit = $real->Length; + $col = $real->Type; + } + + if ($col === 'datetime2') { + return 'datetime'; + } + if (in_array($col, ['date', 'time', 'datetime', 'timestamp'])) { + return $col; + } + if ($col === 'bit') { + return 'boolean'; + } + if (strpos($col, 'bigint') !== false) { + return 'biginteger'; + } + if (strpos($col, 'smallint') !== false) { + return 'smallinteger'; + } + if (strpos($col, 'tinyint') !== false) { + return 'tinyinteger'; + } + if (strpos($col, 'int') !== false) { + return 'integer'; + } + if (strpos($col, 'char') !== false && $limit == -1) { + return 'text'; + } + if (strpos($col, 'char') !== false) { + return 'string'; + } + if (strpos($col, 'text') !== false) { + return 'text'; + } + if (strpos($col, 'binary') !== false || $col === 'image') { + return 'binary'; + } + if (in_array($col, ['float', 'real'])) { + return 'float'; + } + if (in_array($col, ['decimal', 'numeric'])) { + return 'decimal'; + } + return 'text'; + } + + /** + * Handle SQLServer specific length properties. + * SQLServer handles text types as nvarchar/varchar with a length of -1. + * + * @param mixed $length Either the length as a string, or a Column descriptor object. + * @return mixed null|integer with length of column. + */ + public function length($length) + { + if (is_object($length) && isset($length->Length)) { + if ($length->Length == -1 && strpos($length->Type, 'char') !== false) { + return null; + } + if (in_array($length->Type, ['nchar', 'nvarchar'])) { + return floor($length->Length / 2); + } + if ($length->Type === 'text') { + return null; + } + return $length->Length; + } + return parent::length($length); + } + + /** + * Returns a quoted and escaped string of $data for use in an SQL statement. + * + * @param string $data String to be prepared for use in an SQL statement + * @param string $column The column into which this data will be inserted + * @param bool $null Column allows NULL values + * @return string Quoted and escaped data + */ + public function value($data, $column = null, $null = true) + { + if ($data === null || is_array($data) || is_object($data)) { + return parent::value($data, $column, $null); + } + if (in_array($data, ['{$__cakeID__$}', '{$__cakeForeignKey__$}'], true)) { + return $data; + } + + if (empty($column)) { + $column = $this->introspectType($data); + } + + switch ($column) { + case 'string': + case 'text': + return 'N' . $this->_connection->quote($data, PDO::PARAM_STR); + default: + return parent::value($data, $column, $null); + } + } + + /** + * Generates and executes an SQL UPDATE statement for given model, fields, and values. + * Removes Identity (primary key) column from update data before returning to parent. + * + * @param Model $model The model to update. + * @param array $fields The fields to set. + * @param array $values The values to set. + * @param mixed $conditions The conditions to use. + * @return array + */ + public function update(Model $model, $fields = [], $values = null, $conditions = null) + { + if (!empty($values)) { + $fields = array_combine($fields, $values); + } + if (isset($fields[$model->primaryKey])) { + unset($fields[$model->primaryKey]); + } + if (empty($fields)) { + return true; + } + return parent::update($model, array_keys($fields), array_values($fields), $conditions); + } + + /** + * Returns a limit statement in the correct format for the particular database. + * + * @param int $limit Limit of results returned + * @param int $offset Offset from which to start results + * @return string SQL limit/offset statement + */ + public function limit($limit, $offset = null) + { + if ($limit) { + $rt = ''; + if (!strpos(strtolower($limit), 'top') || strpos(strtolower($limit), 'top') === 0) { + $rt = ' TOP'; + } + $rt .= sprintf(' %u', $limit); + if ((is_int($offset) || ctype_digit($offset)) && $offset > 0) { + $rt = sprintf(' OFFSET %u ROWS FETCH FIRST %u ROWS ONLY', $offset, $limit); + } + return $rt; + } + return null; + } + + /** + * Builds a map of the columns contained in a result + * + * @param PDOStatement $results The result to modify. + * @return void + */ + public function resultSet($results) + { + $this->map = []; + $numFields = $results->columnCount(); + $index = 0; + + while ($numFields-- > 0) { + $column = $results->getColumnMeta($index); + $name = $column['name']; + + if (strpos($name, '__')) { + if (isset($this->_fieldMappings[$name]) && strpos($this->_fieldMappings[$name], '.')) { + $map = explode('.', $this->_fieldMappings[$name]); + } else if (isset($this->_fieldMappings[$name])) { + $map = [0, $this->_fieldMappings[$name]]; + } else { + $map = [0, $name]; + } + } else { + $map = [0, $name]; + } + $map[] = ($column['sqlsrv:decl_type'] === 'bit') ? 'boolean' : $column['native_type']; + $this->map[$index++] = $map; + } + } + + /** + * Builds final SQL statement + * + * @param string $type Query type + * @param array $data Query data + * @return string + */ + public function renderStatement($type, $data) + { + switch (strtolower($type)) { + case 'select': + extract($data); + $fields = trim($fields); + + $having = !empty($having) ? " $having" : ''; + $lock = !empty($lock) ? " $lock" : ''; + + if (strpos($limit, 'TOP') !== false && strpos($fields, 'DISTINCT ') === 0) { + $limit = 'DISTINCT ' . trim($limit); + $fields = substr($fields, 9); + } + + // hack order as SQLServer requires an order if there is a limit. + if ($limit && !$order) { + $order = 'ORDER BY (SELECT NULL)'; + } + + // For older versions use the subquery version of pagination. + if (version_compare($this->getVersion(), '11', '<') && preg_match('/FETCH\sFIRST\s+([0-9]+)/i', $limit, $offset)) { + preg_match('/OFFSET\s*(\d+)\s*.*?(\d+)\s*ROWS/', $limit, $limitOffset); + + $limit = 'TOP ' . (int)$limitOffset[2]; + $page = (int)($limitOffset[1] / $limitOffset[2]); + $offset = (int)($limitOffset[2] * $page); + + $rowCounter = static::ROW_COUNTER; + $sql = "SELECT {$limit} * FROM ( SELECT {$fields}, ROW_NUMBER() OVER ({$order}) AS {$rowCounter} FROM {$table} {$alias}{$lock} {$joins} {$conditions} {$group}{$having} ) AS _cake_paging_ WHERE _cake_paging_.{$rowCounter} > {$offset} ORDER BY _cake_paging_.{$rowCounter} "; - return trim($sql); - } - if (strpos($limit, 'FETCH') !== false) { - return trim("SELECT {$fields} FROM {$table} {$alias}{$lock} {$joins} {$conditions} {$group}{$having} {$order} {$limit}"); - } - return trim("SELECT {$limit} {$fields} FROM {$table} {$alias}{$lock} {$joins} {$conditions} {$group}{$having} {$order}"); - case "schema": - extract($data); - - foreach ($indexes as $i => $index) { - if (preg_match('/PRIMARY KEY/', $index)) { - unset($indexes[$i]); - break; - } - } - - foreach (array('columns', 'indexes') as $var) { - if (is_array(${$var})) { - ${$var} = "\t" . implode(",\n\t", array_filter(${$var})); - } - } - return trim("CREATE TABLE {$table} (\n{$columns});\n{$indexes}"); - default: - return parent::renderStatement($type, $data); - } - } - -/** - * Returns a quoted and escaped string of $data for use in an SQL statement. - * - * @param string $data String to be prepared for use in an SQL statement - * @param string $column The column into which this data will be inserted - * @param bool $null Column allows NULL values - * @return string Quoted and escaped data - */ - public function value($data, $column = null, $null = true) { - if ($data === null || is_array($data) || is_object($data)) { - return parent::value($data, $column, $null); - } - if (in_array($data, array('{$__cakeID__$}', '{$__cakeForeignKey__$}'), true)) { - return $data; - } - - if (empty($column)) { - $column = $this->introspectType($data); - } - - switch ($column) { - case 'string': - case 'text': - return 'N' . $this->_connection->quote($data, PDO::PARAM_STR); - default: - return parent::value($data, $column, $null); - } - } - -/** - * Returns an array of all result rows for a given SQL query. - * Returns false if no rows matched. - * - * @param Model $model The model to read from - * @param array $queryData The query data - * @param int $recursive How many layers to go. - * @return array|false Array of resultset rows, or false if no rows matched - */ - public function read(Model $model, $queryData = array(), $recursive = null) { - $results = parent::read($model, $queryData, $recursive); - $this->_fieldMappings = array(); - return $results; - } - -/** - * Fetches the next row from the current result set. - * Eats the magic ROW_COUNTER variable. - * - * @return mixed - */ - public function fetchResult() { - if ($row = $this->_result->fetch(PDO::FETCH_NUM)) { - $resultRow = array(); - foreach ($this->map as $col => $meta) { - list($table, $column, $type) = $meta; - if ($table === 0 && $column === static::ROW_COUNTER) { - continue; - } - $resultRow[$table][$column] = $row[$col]; - if ($type === 'boolean' && $row[$col] !== null) { - $resultRow[$table][$column] = $this->boolean($resultRow[$table][$column]); - } - } - return $resultRow; - } - $this->_result->closeCursor(); - return false; - } - -/** - * Inserts multiple values into a table - * - * @param string $table The table to insert into. - * @param string $fields The fields to set. - * @param array $values The values to set. - * @return void - */ - public function insertMulti($table, $fields, $values) { - $primaryKey = $this->_getPrimaryKey($table); - $hasPrimaryKey = $primaryKey && ( - (is_array($fields) && in_array($primaryKey, $fields) - || (is_string($fields) && strpos($fields, $this->startQuote . $primaryKey . $this->endQuote) !== false)) - ); - - if ($hasPrimaryKey) { - $this->_execute('SET IDENTITY_INSERT ' . $this->fullTableName($table) . ' ON'); - } - - parent::insertMulti($table, $fields, $values); - - if ($hasPrimaryKey) { - $this->_execute('SET IDENTITY_INSERT ' . $this->fullTableName($table) . ' OFF'); - } - } - -/** - * Generate a database-native column schema string - * - * @param array $column An array structured like the - * following: array('name'=>'value', 'type'=>'value'[, options]), - * where options can be 'default', 'length', or 'key'. - * @return string - */ - public function buildColumn($column) { - $result = parent::buildColumn($column); - $result = preg_replace('/(bigint|int|integer)\([0-9]+\)/i', '$1', $result); - $result = preg_replace('/(bit)\([0-9]+\)/i', '$1', $result); - if (strpos($result, 'DEFAULT NULL') !== false) { - if (isset($column['default']) && $column['default'] === '') { - $result = str_replace('DEFAULT NULL', "DEFAULT ''", $result); - } else { - $result = str_replace('DEFAULT NULL', 'NULL', $result); - } - } elseif (array_keys($column) === array('type', 'name')) { - $result .= ' NULL'; - } elseif (strpos($result, "DEFAULT N'")) { - $result = str_replace("DEFAULT N'", "DEFAULT '", $result); - } - return $result; - } - -/** - * Format indexes for create table - * - * @param array $indexes The indexes to build - * @param string $table The table to make indexes for. - * @return string - */ - public function buildIndex($indexes, $table = null) { - $join = array(); - - foreach ($indexes as $name => $value) { - if ($name === 'PRIMARY') { - $join[] = 'PRIMARY KEY (' . $this->name($value['column']) . ')'; - } elseif (isset($value['unique']) && $value['unique']) { - $out = "ALTER TABLE {$table} ADD CONSTRAINT {$name} UNIQUE"; - - if (is_array($value['column'])) { - $value['column'] = implode(', ', array_map(array(&$this, 'name'), $value['column'])); - } else { - $value['column'] = $this->name($value['column']); - } - $out .= "({$value['column']});"; - $join[] = $out; - } - } - return $join; - } - -/** - * Makes sure it will return the primary key - * - * @param Model|string $model Model instance of table name - * @return string - */ - protected function _getPrimaryKey($model) { - $schema = $this->describe($model); - foreach ($schema as $field => $props) { - if (isset($props['key']) && $props['key'] === 'primary') { - return $field; - } - } - return null; - } - -/** - * Returns number of affected rows in previous database operation. If no previous operation exists, - * this returns false. - * - * @param mixed $source Unused - * @return int Number of affected rows - */ - public function lastAffected($source = null) { - $affected = parent::lastAffected(); - if ($affected === null && $this->_lastAffected !== false) { - return $this->_lastAffected; - } - return $affected; - } - -/** - * Executes given SQL statement. - * - * @param string $sql SQL statement - * @param array $params list of params to be bound to query (supported only in select) - * @param array $prepareOptions Options to be used in the prepare statement - * @return mixed PDOStatement if query executes with no problem, true as the result of a successful, false on error - * query returning no rows, such as a CREATE statement, false otherwise - * @throws PDOException - */ - protected function _execute($sql, $params = array(), $prepareOptions = array()) { - $this->_lastAffected = false; - $sql = trim($sql); - if (strncasecmp($sql, 'SELECT', 6) === 0 || preg_match('/^EXEC(?:UTE)?\s/mi', $sql) > 0) { - $prepareOptions += array(PDO::ATTR_CURSOR => PDO::CURSOR_SCROLL); - return parent::_execute($sql, $params, $prepareOptions); - } - try { - $this->_lastAffected = $this->_connection->exec($sql); - if ($this->_lastAffected === false) { - $this->_results = null; - $error = $this->_connection->errorInfo(); - $this->error = $error[2]; - return false; - } - return true; - } catch (PDOException $e) { - if (isset($query->queryString)) { - $e->queryString = $query->queryString; - } else { - $e->queryString = $sql; - } - throw $e; - } - } - -/** - * Generate a "drop table" statement for the given table - * - * @param type $table Name of the table to drop - * @return string Drop table SQL statement - */ - protected function _dropTable($table) { - return "IF OBJECT_ID('" . $this->fullTableName($table, false) . "', 'U') IS NOT NULL DROP TABLE " . $this->fullTableName($table) . ";"; - } - -/** - * Gets the schema name - * - * @return string The schema name - */ - public function getSchemaName() { - return $this->config['schema']; - } - -/** - * Returns a locking hint for the given mode. - * - * Currently, this method only returns WITH (UPDLOCK) when the mode is set to true. - * - * @param mixed $mode Lock mode - * @return string|null WITH (UPDLOCK) clause or null - */ - public function getLockingHint($mode) { - if ($mode !== true) { - return null; - } - return ' WITH (UPDLOCK)'; - } + return trim($sql); + } + if (strpos($limit, 'FETCH') !== false) { + return trim("SELECT {$fields} FROM {$table} {$alias}{$lock} {$joins} {$conditions} {$group}{$having} {$order} {$limit}"); + } + return trim("SELECT {$limit} {$fields} FROM {$table} {$alias}{$lock} {$joins} {$conditions} {$group}{$having} {$order}"); + case "schema": + extract($data); + + foreach ($indexes as $i => $index) { + if (preg_match('/PRIMARY KEY/', $index)) { + unset($indexes[$i]); + break; + } + } + + foreach (['columns', 'indexes'] as $var) { + if (is_array(${$var})) { + ${$var} = "\t" . implode(",\n\t", array_filter(${$var})); + } + } + return trim("CREATE TABLE {$table} (\n{$columns});\n{$indexes}"); + default: + return parent::renderStatement($type, $data); + } + } + + /** + * Returns an array of all result rows for a given SQL query. + * Returns false if no rows matched. + * + * @param Model $model The model to read from + * @param array $queryData The query data + * @param int $recursive How many layers to go. + * @return array|false Array of resultset rows, or false if no rows matched + */ + public function read(Model $model, $queryData = [], $recursive = null) + { + $results = parent::read($model, $queryData, $recursive); + $this->_fieldMappings = []; + return $results; + } + + /** + * Fetches the next row from the current result set. + * Eats the magic ROW_COUNTER variable. + * + * @return mixed + */ + public function fetchResult() + { + if ($row = $this->_result->fetch(PDO::FETCH_NUM)) { + $resultRow = []; + foreach ($this->map as $col => $meta) { + list($table, $column, $type) = $meta; + if ($table === 0 && $column === static::ROW_COUNTER) { + continue; + } + $resultRow[$table][$column] = $row[$col]; + if ($type === 'boolean' && $row[$col] !== null) { + $resultRow[$table][$column] = $this->boolean($resultRow[$table][$column]); + } + } + return $resultRow; + } + $this->_result->closeCursor(); + return false; + } + + /** + * Inserts multiple values into a table + * + * @param string $table The table to insert into. + * @param string $fields The fields to set. + * @param array $values The values to set. + * @return void + */ + public function insertMulti($table, $fields, $values) + { + $primaryKey = $this->_getPrimaryKey($table); + $hasPrimaryKey = $primaryKey && ( + (is_array($fields) && in_array($primaryKey, $fields) + || (is_string($fields) && strpos($fields, $this->startQuote . $primaryKey . $this->endQuote) !== false)) + ); + + if ($hasPrimaryKey) { + $this->_execute('SET IDENTITY_INSERT ' . $this->fullTableName($table) . ' ON'); + } + + parent::insertMulti($table, $fields, $values); + + if ($hasPrimaryKey) { + $this->_execute('SET IDENTITY_INSERT ' . $this->fullTableName($table) . ' OFF'); + } + } + + /** + * Generate a database-native column schema string + * + * @param array $column An array structured like the + * following: array('name'=>'value', 'type'=>'value'[, options]), + * where options can be 'default', 'length', or 'key'. + * @return string + */ + public function buildColumn($column) + { + $result = parent::buildColumn($column); + $result = preg_replace('/(bigint|int|integer)\([0-9]+\)/i', '$1', $result); + $result = preg_replace('/(bit)\([0-9]+\)/i', '$1', $result); + if (strpos($result, 'DEFAULT NULL') !== false) { + if (isset($column['default']) && $column['default'] === '') { + $result = str_replace('DEFAULT NULL', "DEFAULT ''", $result); + } else { + $result = str_replace('DEFAULT NULL', 'NULL', $result); + } + } else if (array_keys($column) === ['type', 'name']) { + $result .= ' NULL'; + } else if (strpos($result, "DEFAULT N'")) { + $result = str_replace("DEFAULT N'", "DEFAULT '", $result); + } + return $result; + } + + /** + * Format indexes for create table + * + * @param array $indexes The indexes to build + * @param string $table The table to make indexes for. + * @return string + */ + public function buildIndex($indexes, $table = null) + { + $join = []; + + foreach ($indexes as $name => $value) { + if ($name === 'PRIMARY') { + $join[] = 'PRIMARY KEY (' . $this->name($value['column']) . ')'; + } else if (isset($value['unique']) && $value['unique']) { + $out = "ALTER TABLE {$table} ADD CONSTRAINT {$name} UNIQUE"; + + if (is_array($value['column'])) { + $value['column'] = implode(', ', array_map([&$this, 'name'], $value['column'])); + } else { + $value['column'] = $this->name($value['column']); + } + $out .= "({$value['column']});"; + $join[] = $out; + } + } + return $join; + } + + /** + * Returns number of affected rows in previous database operation. If no previous operation exists, + * this returns false. + * + * @param mixed $source Unused + * @return int Number of affected rows + */ + public function lastAffected($source = null) + { + $affected = parent::lastAffected(); + if ($affected === null && $this->_lastAffected !== false) { + return $this->_lastAffected; + } + return $affected; + } + + /** + * Gets the schema name + * + * @return string The schema name + */ + public function getSchemaName() + { + return $this->config['schema']; + } + + /** + * Returns a locking hint for the given mode. + * + * Currently, this method only returns WITH (UPDLOCK) when the mode is set to true. + * + * @param mixed $mode Lock mode + * @return string|null WITH (UPDLOCK) clause or null + */ + public function getLockingHint($mode) + { + if ($mode !== true) { + return null; + } + return ' WITH (UPDLOCK)'; + } + + /** + * Generate a "drop table" statement for the given table + * + * @param type $table Name of the table to drop + * @return string Drop table SQL statement + */ + protected function _dropTable($table) + { + return "IF OBJECT_ID('" . $this->fullTableName($table, false) . "', 'U') IS NOT NULL DROP TABLE " . $this->fullTableName($table) . ";"; + } } diff --git a/lib/Cake/Model/Datasource/DboSource.php b/lib/Cake/Model/Datasource/DboSource.php index ebfab0a9..05d2bf4f 100755 --- a/lib/Cake/Model/Datasource/DboSource.php +++ b/lib/Cake/Model/Datasource/DboSource.php @@ -27,3727 +27,3808 @@ * * @package Cake.Model.Datasource */ -class DboSource extends DataSource { - -/** - * Description string for this Database Data Source. - * - * @var string - */ - public $description = "Database Data Source"; - -/** - * index definition, standard cake, primary, index, unique - * - * @var array - */ - public $index = array('PRI' => 'primary', 'MUL' => 'index', 'UNI' => 'unique'); - -/** - * Database keyword used to assign aliases to identifiers. - * - * @var string - */ - public $alias = 'AS '; - -/** - * Caches result from query parsing operations. Cached results for both DboSource::name() and DboSource::fields() - * will be stored here. - * - * Method caching uses `md5` (by default) to construct cache keys. If you have problems with collisions, - * try a different hashing algorithm by overriding DboSource::cacheMethodHasher or set DboSource::$cacheMethods to false. - * - * @var array - */ - public static $methodCache = array(); - -/** - * Whether or not to cache the results of DboSource::name() and DboSource::fields() into the memory cache. - * Set to false to disable the use of the memory cache. - * - * @var bool - */ - public $cacheMethods = true; - -/** - * Flag to support nested transactions. If it is set to false, you will be able to use - * the transaction methods (begin/commit/rollback), but just the global transaction will - * be executed. - * - * @var bool - */ - public $useNestedTransactions = false; - -/** - * Print full query debug info? - * - * @var bool - */ - public $fullDebug = false; - -/** - * String to hold how many rows were affected by the last SQL operation. - * - * @var string - */ - public $affected = null; - -/** - * Number of rows in current resultset - * - * @var int - */ - public $numRows = null; - -/** - * Time the last query took - * - * @var int - */ - public $took = null; - -/** - * Result - * - * @var array|PDOStatement - */ - protected $_result = null; - -/** - * Queries count. - * - * @var int - */ - protected $_queriesCnt = 0; - -/** - * Total duration of all queries. - * - * @var int - */ - protected $_queriesTime = null; - -/** - * Log of queries executed by this DataSource - * - * @var array - */ - protected $_queriesLog = array(); - -/** - * Maximum number of items in query log - * - * This is to prevent query log taking over too much memory. - * - * @var int - */ - protected $_queriesLogMax = 200; - -/** - * Caches serialized results of executed queries - * - * @var array - */ - protected $_queryCache = array(); - -/** - * A reference to the physical connection of this DataSource - * - * @var array - */ - protected $_connection = null; - -/** - * The DataSource configuration key name - * - * @var string - */ - public $configKeyName = null; - -/** - * The starting character that this DataSource uses for quoted identifiers. - * - * @var string - */ - public $startQuote = null; - -/** - * The ending character that this DataSource uses for quoted identifiers. - * - * @var string - */ - public $endQuote = null; - -/** - * The set of valid SQL operations usable in a WHERE statement - * - * @var array - */ - protected $_sqlOps = array('like', 'ilike', 'rlike', 'or', 'not', 'in', 'between', 'regexp', 'similar to'); - -/** - * The set of valid SQL boolean operations usable in a WHERE statement - * - * @var array - */ - protected $_sqlBoolOps = array('and', 'or', 'not', 'and not', 'or not', 'xor', '||', '&&'); - -/** - * Indicates the level of nested transactions - * - * @var int - */ - protected $_transactionNesting = 0; - -/** - * Default fields that are used by the DBO - * - * @var array - */ - protected $_queryDefaults = array( - 'conditions' => array(), - 'fields' => null, - 'table' => null, - 'alias' => null, - 'order' => null, - 'limit' => null, - 'joins' => array(), - 'group' => null, - 'offset' => null, - 'having' => null, - 'lock' => null, - ); - -/** - * Separator string for virtualField composition - * - * @var string - */ - public $virtualFieldSeparator = '__'; - -/** - * List of table engine specific parameters used on table creating - * - * @var array - */ - public $tableParameters = array(); - -/** - * List of engine specific additional field parameters used on table creating - * - * @var array - */ - public $fieldParameters = array(); - -/** - * Indicates whether there was a change on the cached results on the methods of this class - * This will be used for storing in a more persistent cache - * - * @var bool - */ - protected $_methodCacheChange = false; - -/** - * Map of the columns contained in a result. - * - * @var array - */ - public $map = array(); - -/** - * Constructor - * - * @param array $config Array of configuration information for the Datasource. - * @param bool $autoConnect Whether or not the datasource should automatically connect. - * @throws MissingConnectionException when a connection cannot be made. - */ - public function __construct($config = null, $autoConnect = true) { - if (!isset($config['prefix'])) { - $config['prefix'] = ''; - } - parent::__construct($config); - $this->fullDebug = Configure::read('debug') > 1; - if (!$this->enabled()) { - throw new MissingConnectionException(array( - 'class' => get_class($this), - 'message' => __d('cake_dev', 'Selected driver is not enabled'), - 'enabled' => false - )); - } - if ($autoConnect) { - $this->connect(); - } - } - -/** - * Connects to the database. - * - * @return bool - */ - public function connect() { - // This method is implemented in subclasses - return $this->connected; - } - -/** - * Reconnects to database server with optional new settings - * - * @param array $config An array defining the new configuration settings - * @return bool True on success, false on failure - */ - public function reconnect($config = array()) { - $this->disconnect(); - $this->setConfig($config); - $this->_sources = null; - - return $this->connect(); - } - -/** - * Disconnects from database. - * - * @return bool Always true - */ - public function disconnect() { - if ($this->_result instanceof PDOStatement) { - $this->_result->closeCursor(); - } - $this->_connection = null; - $this->connected = false; - return true; - } - -/** - * Get the underlying connection object. - * - * @return PDO - */ - public function getConnection() { - return $this->_connection; - } - -/** - * Gets the version string of the database server - * - * @return string The database version - */ - public function getVersion() { - return $this->_connection->getAttribute(PDO::ATTR_SERVER_VERSION); - } - -/** - * Returns a quoted and escaped string of $data for use in an SQL statement. - * - * @param string $data String to be prepared for use in an SQL statement - * @param string $column The column datatype into which this data will be inserted. - * @param bool $null Column allows NULL values - * @return string Quoted and escaped data - */ - public function value($data, $column = null, $null = true) { - if (is_array($data) && !empty($data)) { - return array_map( - array(&$this, 'value'), - $data, array_fill(0, count($data), $column) - ); - } elseif (is_object($data) && isset($data->type, $data->value)) { - if ($data->type === 'identifier') { - return $this->name($data->value); - } elseif ($data->type === 'expression') { - return $data->value; - } - } elseif (in_array($data, array('{$__cakeID__$}', '{$__cakeForeignKey__$}'), true)) { - return $data; - } - - if ($data === null || (is_array($data) && empty($data))) { - return 'NULL'; - } - - if (empty($column)) { - $column = $this->introspectType($data); - } - - $isStringEnum = false; - if (strpos($column, "enum") === 0) { - $firstValue = null; - if (preg_match("/(enum\()(.*)(\))/i", $column, $acceptingValues)) { - $values = explode(",", $acceptingValues[2]); - $firstValue = $values[0]; - } - if (is_string($firstValue)) { - $isStringEnum = true; - } - } - - switch ($column) { - case 'binary': - return $this->_connection->quote($data, PDO::PARAM_LOB); - case 'boolean': - return $this->_connection->quote($this->boolean($data, true), PDO::PARAM_BOOL); - case 'string': - case 'text': - return $this->_connection->quote($data, PDO::PARAM_STR); - default: - if ($data === '') { - return $null ? 'NULL' : '""'; - } - if (is_float($data)) { - return str_replace(',', '.', strval($data)); - } - if (((is_int($data) || $data === '0') || ( - is_numeric($data) && - strpos($data, ',') === false && - $data[0] != '0' && - strpos($data, 'e') === false) - ) && !$isStringEnum - ) { - return $data; - } - return $this->_connection->quote($data); - } - } - -/** - * Returns an object to represent a database identifier in a query. Expression objects - * are not sanitized or escaped. - * - * @param string $identifier A SQL expression to be used as an identifier - * @return stdClass An object representing a database identifier to be used in a query - */ - public function identifier($identifier) { - $obj = new stdClass(); - $obj->type = 'identifier'; - $obj->value = $identifier; - return $obj; - } - -/** - * Returns an object to represent a database expression in a query. Expression objects - * are not sanitized or escaped. - * - * @param string $expression An arbitrary SQL expression to be inserted into a query. - * @return stdClass An object representing a database expression to be used in a query - */ - public function expression($expression) { - $obj = new stdClass(); - $obj->type = 'expression'; - $obj->value = $expression; - return $obj; - } - -/** - * Executes given SQL statement. - * - * @param string $sql SQL statement - * @param array $params Additional options for the query. - * @return mixed Resource or object representing the result set, or false on failure - */ - public function rawQuery($sql, $params = array()) { - $this->took = $this->numRows = false; - return $this->execute($sql, array(), $params); - } - -/** - * Queries the database with given SQL statement, and obtains some metadata about the result - * (rows affected, timing, any errors, number of rows in resultset). The query is also logged. - * If Configure::read('debug') is set, the log is shown all the time, else it is only shown on errors. - * - * ### Options - * - * - log - Whether or not the query should be logged to the memory log. - * - * @param string $sql SQL statement - * @param array $options The options for executing the query. - * @param array $params values to be bound to the query. - * @return mixed Resource or object representing the result set, or false on failure - */ - public function execute($sql, $options = array(), $params = array()) { - $options += array('log' => $this->fullDebug); - - $t = microtime(true); - $this->_result = $this->_execute($sql, $params); - - if ($options['log']) { - $this->took = round((microtime(true) - $t) * 1000, 0); - $this->numRows = $this->affected = $this->lastAffected(); - $this->logQuery($sql, $params); - } - - return $this->_result; - } - -/** - * Executes given SQL statement. - * - * @param string $sql SQL statement - * @param array $params list of params to be bound to query - * @param array $prepareOptions Options to be used in the prepare statement - * @return mixed PDOStatement if query executes with no problem, true as the result of a successful, false on error - * query returning no rows, such as a CREATE statement, false otherwise - * @throws PDOException - */ - protected function _execute($sql, $params = array(), $prepareOptions = array()) { - $sql = trim($sql); - if (preg_match('/^(?:CREATE|ALTER|DROP)\s+(?:TABLE|INDEX)/i', $sql)) { - $statements = array_filter(explode(';', $sql)); - if (count($statements) > 1) { - $result = array_map(array($this, '_execute'), $statements); - return array_search(false, $result) === false; - } - } - - try { - $query = $this->_connection->prepare($sql, $prepareOptions); - $query->setFetchMode(PDO::FETCH_LAZY); - if (!$query->execute($params)) { - $this->_result = $query; - $query->closeCursor(); - return false; - } - if (!$query->columnCount()) { - $query->closeCursor(); - if (!$query->rowCount()) { - return true; - } - } - return $query; - } catch (PDOException $e) { - if (isset($query->queryString)) { - $e->queryString = $query->queryString; - } else { - $e->queryString = $sql; - } - throw $e; - } - } - -/** - * Returns a formatted error message from previous database operation. - * - * @param PDOStatement $query the query to extract the error from if any - * @return string Error message with error number - */ - public function lastError(PDOStatement $query = null) { - if ($query) { - $error = $query->errorInfo(); - } else { - $error = $this->_connection->errorInfo(); - } - if (empty($error[2])) { - return null; - } - return $error[1] . ': ' . $error[2]; - } - -/** - * Returns number of affected rows in previous database operation. If no previous operation exists, - * this returns false. - * - * @param mixed $source The source to check. - * @return int Number of affected rows - */ - public function lastAffected($source = null) { - if ($this->hasResult()) { - return $this->_result->rowCount(); - } - return 0; - } - -/** - * Returns number of rows in previous resultset. If no previous resultset exists, - * this returns false. - * - * @param mixed $source Not used - * @return int Number of rows in resultset - */ - public function lastNumRows($source = null) { - return $this->lastAffected(); - } - -/** - * DataSource Query abstraction - * - * @return resource Result resource identifier. - */ - public function query() { - $args = func_get_args(); - $fields = null; - $order = null; - $limit = null; - $page = null; - $recursive = null; - - if (count($args) === 1) { - return $this->fetchAll($args[0]); - } elseif (count($args) > 1 && preg_match('/^find(\w*)By(.+)/', $args[0], $matches)) { - $params = $args[1]; - - $findType = lcfirst($matches[1]); - $field = Inflector::underscore($matches[2]); - - $or = (strpos($field, '_or_') !== false); - if ($or) { - $field = explode('_or_', $field); - } else { - $field = explode('_and_', $field); - } - $off = count($field) - 1; - - if (isset($params[1 + $off])) { - $fields = $params[1 + $off]; - } - - if (isset($params[2 + $off])) { - $order = $params[2 + $off]; - } - - if (!array_key_exists(0, $params)) { - return false; - } - - $c = 0; - $conditions = array(); - - foreach ($field as $f) { - $conditions[$args[2]->alias . '.' . $f] = $params[$c++]; - } - - if ($or) { - $conditions = array('OR' => $conditions); - } - - if ($findType !== 'first' && $findType !== '') { - if (isset($params[3 + $off])) { - $limit = $params[3 + $off]; - } - - if (isset($params[4 + $off])) { - $page = $params[4 + $off]; - } - - if (isset($params[5 + $off])) { - $recursive = $params[5 + $off]; - } - return $args[2]->find($findType, compact('conditions', 'fields', 'order', 'limit', 'page', 'recursive')); - } - if (isset($params[3 + $off])) { - $recursive = $params[3 + $off]; - } - return $args[2]->find('first', compact('conditions', 'fields', 'order', 'recursive')); - } - if (isset($args[1]) && $args[1] === true) { - return $this->fetchAll($args[0], true); - } elseif (isset($args[1]) && !is_array($args[1])) { - return $this->fetchAll($args[0], false); - } elseif (isset($args[1]) && is_array($args[1])) { - if (isset($args[2])) { - $cache = $args[2]; - } else { - $cache = true; - } - return $this->fetchAll($args[0], $args[1], array('cache' => $cache)); - } - } - -/** - * Builds a map of the columns contained in a result - * - * @param PDOStatement $results The results to format. - * @return void - */ - public function resultSet($results) { - // This method is implemented in subclasses - } - -/** - * Returns a row from current resultset as an array - * - * @param string $sql Some SQL to be executed. - * @return array The fetched row as an array - */ - public function fetchRow($sql = null) { - if (is_string($sql) && strlen($sql) > 5 && !$this->execute($sql)) { - return null; - } - - if ($this->hasResult()) { - $this->resultSet($this->_result); - $resultRow = $this->fetchResult(); - if (isset($resultRow[0])) { - $this->fetchVirtualField($resultRow); - } - return $resultRow; - } - return null; - } - -/** - * Returns an array of all result rows for a given SQL query. - * - * Returns false if no rows matched. - * - * ### Options - * - * - `cache` - Returns the cached version of the query, if exists and stores the result in cache. - * This is a non-persistent cache, and only lasts for a single request. This option - * defaults to true. If you are directly calling this method, you can disable caching - * by setting $options to `false` - * - * @param string $sql SQL statement - * @param array|bool $params Either parameters to be bound as values for the SQL statement, - * or a boolean to control query caching. - * @param array $options additional options for the query. - * @return bool|array Array of resultset rows, or false if no rows matched - */ - public function fetchAll($sql, $params = array(), $options = array()) { - if (is_string($options)) { - $options = array('modelName' => $options); - } - if (is_bool($params)) { - $options['cache'] = $params; - $params = array(); - } - $options += array('cache' => true); - $cache = $options['cache']; - if ($cache && ($cached = $this->getQueryCache($sql, $params)) !== false) { - return $cached; - } - $result = $this->execute($sql, array(), $params); - if ($result) { - $out = array(); - - if ($this->hasResult()) { - $first = $this->fetchRow(); - if ($first) { - $out[] = $first; - } - while ($item = $this->fetchResult()) { - if (isset($item[0])) { - $this->fetchVirtualField($item); - } - $out[] = $item; - } - } - - if (!is_bool($result) && $cache) { - $this->_writeQueryCache($sql, $out, $params); - } - - if (empty($out) && is_bool($this->_result)) { - return $this->_result; - } - return $out; - } - return false; - } - -/** - * Fetches the next row from the current result set - * - * @return bool - */ - public function fetchResult() { - return false; - } - -/** - * Modifies $result array to place virtual fields in model entry where they belongs to - * - * @param array &$result Reference to the fetched row - * @return void - */ - public function fetchVirtualField(&$result) { - if (isset($result[0]) && is_array($result[0])) { - foreach ($result[0] as $field => $value) { - if (strpos($field, $this->virtualFieldSeparator) === false) { - continue; - } - - list($alias, $virtual) = explode($this->virtualFieldSeparator, $field); - - if (!ClassRegistry::isKeySet($alias)) { - return; - } - - $Model = ClassRegistry::getObject($alias); - - if ($Model->isVirtualField($virtual)) { - $result[$alias][$virtual] = $value; - unset($result[0][$field]); - } - } - if (empty($result[0])) { - unset($result[0]); - } - } - } - -/** - * Returns a single field of the first of query results for a given SQL query, or false if empty. - * - * @param string $name The name of the field to get. - * @param string $sql The SQL query. - * @return mixed Value of field read, or false if not found. - */ - public function field($name, $sql) { - $data = $this->fetchRow($sql); - if (empty($data[$name])) { - return false; - } - return $data[$name]; - } - -/** - * Empties the method caches. - * These caches are used by DboSource::name() and DboSource::conditions() - * - * @return void - */ - public function flushMethodCache() { - $this->_methodCacheChange = true; - static::$methodCache = array(); - } - -/** - * Cache a value into the methodCaches. Will respect the value of DboSource::$cacheMethods. - * Will retrieve a value from the cache if $value is null. - * - * If caching is disabled and a write is attempted, the $value will be returned. - * A read will either return the value or null. - * - * @param string $method Name of the method being cached. - * @param string $key The key name for the cache operation. - * @param mixed $value The value to cache into memory. - * @return mixed Either null on failure, or the value if its set. - */ - public function cacheMethod($method, $key, $value = null) { - if ($this->cacheMethods === false) { - return $value; - } - if (!$this->_methodCacheChange && empty(static::$methodCache)) { - static::$methodCache = (array)Cache::read('method_cache', '_cake_core_'); - } - if ($value === null) { - return (isset(static::$methodCache[$method][$key])) ? static::$methodCache[$method][$key] : null; - } - if (!$this->cacheMethodFilter($method, $key, $value)) { - return $value; - } - $this->_methodCacheChange = true; - return static::$methodCache[$method][$key] = $value; - } - -/** - * Filters to apply to the results of `name` and `fields`. When the filter for a given method does not return `true` - * then the result is not added to the memory cache. - * - * Some examples: - * - * ``` - * // For method fields, do not cache values that contain floats - * if ($method === 'fields') { - * $hasFloat = preg_grep('/(\d+)?\.\d+/', $value); - * - * return count($hasFloat) === 0; - * } - * - * return true; - * ``` - * - * ``` - * // For method name, do not cache values that have the name created - * if ($method === 'name') { - * return preg_match('/^`created`$/', $value) !== 1; - * } - * - * return true; - * ``` - * - * ``` - * // For method name, do not cache values that have the key 472551d38e1f8bbc78d7dfd28106166f - * if ($key === '472551d38e1f8bbc78d7dfd28106166f') { - * return false; - * } - * - * return true; - * ``` - * - * @param string $method Name of the method being cached. - * @param string $key The key name for the cache operation. - * @param mixed $value The value to cache into memory. - * @return bool Whether or not to cache - */ - public function cacheMethodFilter($method, $key, $value) { - return true; - } - -/** - * Hashes a given value. - * - * Method caching uses `md5` (by default) to construct cache keys. If you have problems with collisions, - * try a different hashing algorithm or set DboSource::$cacheMethods to false. - * - * @param string $value Value to hash - * @return string Hashed value - * @see http://php.net/manual/en/function.hash-algos.php - * @see http://softwareengineering.stackexchange.com/questions/49550/which-hashing-algorithm-is-best-for-uniqueness-and-speed - */ - public function cacheMethodHasher($value) { - return md5($value); - } - -/** - * Returns a quoted name of $data for use in an SQL statement. - * Strips fields out of SQL functions before quoting. - * - * Results of this method are stored in a memory cache. This improves performance, but - * because the method uses a hashing algorithm it can have collisions. - * Setting DboSource::$cacheMethods to false will disable the memory cache. - * - * @param mixed $data Either a string with a column to quote. An array of columns to quote or an - * object from DboSource::expression() or DboSource::identifier() - * @return string SQL field - */ - public function name($data) { - if (is_object($data) && isset($data->type)) { - return $data->value; - } - if ($data === '*') { - return '*'; - } - if (is_array($data)) { - foreach ($data as $i => $dataItem) { - $data[$i] = $this->name($dataItem); - } - return $data; - } - $cacheKey = $this->cacheMethodHasher($this->startQuote . $data . $this->endQuote); - if ($return = $this->cacheMethod(__FUNCTION__, $cacheKey)) { - return $return; - } - $data = trim($data); - if (preg_match('/^[\w-]+(?:\.[^ \*]*)*$/', $data)) { // string, string.string - if (strpos($data, '.') === false) { // string - return $this->cacheMethod(__FUNCTION__, $cacheKey, $this->startQuote . $data . $this->endQuote); - } - $items = explode('.', $data); - return $this->cacheMethod(__FUNCTION__, $cacheKey, - $this->startQuote . implode($this->endQuote . '.' . $this->startQuote, $items) . $this->endQuote - ); - } - if (preg_match('/^[\w-]+\.\*$/', $data)) { // string.* - return $this->cacheMethod(__FUNCTION__, $cacheKey, - $this->startQuote . str_replace('.*', $this->endQuote . '.*', $data) - ); - } - if (preg_match('/^([\w-]+)\((.*)\)$/', $data, $matches)) { // Functions - return $this->cacheMethod(__FUNCTION__, $cacheKey, - $matches[1] . '(' . $this->name($matches[2]) . ')' - ); - } - if (preg_match('/^([\w-]+(\.[\w-]+|\(.*\))*)\s+' . preg_quote($this->alias) . '\s*([\w-]+)$/i', $data, $matches)) { - return $this->cacheMethod( - __FUNCTION__, $cacheKey, - preg_replace( - '/\s{2,}/', ' ', $this->name($matches[1]) . ' ' . $this->alias . ' ' . $this->name($matches[3]) - ) - ); - } - if (preg_match('/^[\w\-_\s]*[\w\-_]+/', $data)) { - return $this->cacheMethod(__FUNCTION__, $cacheKey, $this->startQuote . $data . $this->endQuote); - } - return $this->cacheMethod(__FUNCTION__, $cacheKey, $data); - } - -/** - * Checks if the source is connected to the database. - * - * @return bool True if the database is connected, else false - */ - public function isConnected() { - if ($this->_connection === null) { - $connected = false; - } else { - try { - $connected = $this->_connection->query('SELECT 1'); - } catch (Exception $e) { - $connected = false; - } - } - $this->connected = !empty($connected); - return $this->connected; - } - -/** - * Checks if the result is valid - * - * @return bool True if the result is valid else false - */ - public function hasResult() { - return $this->_result instanceof PDOStatement; - } - -/** - * Get the query log as an array. - * - * @param bool $sorted Get the queries sorted by time taken, defaults to false. - * @param bool $clear If True the existing log will cleared. - * @return array Array of queries run as an array - */ - public function getLog($sorted = false, $clear = true) { - if ($sorted) { - $log = sortByKey($this->_queriesLog, 'took', 'desc', SORT_NUMERIC); - } else { - $log = $this->_queriesLog; - } - if ($clear) { - $this->_queriesLog = array(); - } - return array('log' => $log, 'count' => $this->_queriesCnt, 'time' => $this->_queriesTime); - } - -/** - * Outputs the contents of the queries log. If in a non-CLI environment the sql_log element - * will be rendered and output. If in a CLI environment, a plain text log is generated. - * - * @param bool $sorted Get the queries sorted by time taken, defaults to false. - * @return void - */ - public function showLog($sorted = false) { - $log = $this->getLog($sorted, false); - if (empty($log['log'])) { - return; - } - if (PHP_SAPI !== 'cli') { - $controller = null; - $View = new View($controller, false); - $View->set('sqlLogs', array($this->configKeyName => $log)); - echo $View->element('sql_dump', array('_forced_from_dbo_' => true)); - } else { - foreach ($log['log'] as $k => $i) { - print (($k + 1) . ". {$i['query']}\n"); - } - } - } - -/** - * Log given SQL query. - * - * @param string $sql SQL statement - * @param array $params Values binded to the query (prepared statements) - * @return void - */ - public function logQuery($sql, $params = array()) { - $this->_queriesCnt++; - $this->_queriesTime += $this->took; - $this->_queriesLog[] = array( - 'query' => $sql, - 'params' => $params, - 'affected' => $this->affected, - 'numRows' => $this->numRows, - 'took' => $this->took - ); - if (count($this->_queriesLog) > $this->_queriesLogMax) { - array_shift($this->_queriesLog); - } - } - -/** - * Gets full table name including prefix - * - * @param Model|string $model Either a Model object or a string table name. - * @param bool $quote Whether you want the table name quoted. - * @param bool $schema Whether you want the schema name included. - * @return string Full quoted table name - */ - public function fullTableName($model, $quote = true, $schema = true) { - if (is_object($model)) { - $schemaName = $model->schemaName; - $table = $model->tablePrefix . $model->table; - } elseif (!empty($this->config['prefix']) && strpos($model, $this->config['prefix']) !== 0) { - $table = $this->config['prefix'] . strval($model); - } else { - $table = strval($model); - } - - if ($schema && !isset($schemaName)) { - $schemaName = $this->getSchemaName(); - } - - if ($quote) { - if ($schema && !empty($schemaName)) { - if (strstr($table, '.') === false) { - return $this->name($schemaName) . '.' . $this->name($table); - } - } - return $this->name($table); - } - - if ($schema && !empty($schemaName)) { - if (strstr($table, '.') === false) { - return $schemaName . '.' . $table; - } - } - - return $table; - } - -/** - * The "C" in CRUD - * - * Creates new records in the database. - * - * @param Model $Model Model object that the record is for. - * @param array $fields An array of field names to insert. If null, $Model->data will be - * used to generate field names. - * @param array $values An array of values with keys matching the fields. If null, $Model->data will - * be used to generate values. - * @return bool Success - */ - public function create(Model $Model, $fields = null, $values = null) { - $id = null; - - if (!$fields) { - unset($fields, $values); - $fields = array_keys($Model->data); - $values = array_values($Model->data); - } - $count = count($fields); - - for ($i = 0; $i < $count; $i++) { - $schema = $Model->schema(); - $valueInsert[] = $this->value($values[$i], $Model->getColumnType($fields[$i]), isset($schema[$fields[$i]]['null']) ? $schema[$fields[$i]]['null'] : true); - $fieldInsert[] = $this->name($fields[$i]); - if ($fields[$i] === $Model->primaryKey) { - $id = $values[$i]; - } - } - - $query = array( - 'table' => $this->fullTableName($Model), - 'fields' => implode(', ', $fieldInsert), - 'values' => implode(', ', $valueInsert) - ); - - if ($this->execute($this->renderStatement('create', $query))) { - if (empty($id)) { - $id = $this->lastInsertId($this->fullTableName($Model, false, false), $Model->primaryKey); - } - $Model->setInsertID($id); - $Model->id = $id; - return true; - } - - $Model->onError(); - return false; - } - -/** - * The "R" in CRUD - * - * Reads record(s) from the database. - * - * @param Model $Model A Model object that the query is for. - * @param array $queryData An array of queryData information containing keys similar to Model::find(). - * @param int $recursive Number of levels of association - * @return mixed boolean false on error/failure. An array of results on success. - */ - public function read(Model $Model, $queryData = array(), $recursive = null) { - $queryData = $this->_scrubQueryData($queryData); - - $array = array('callbacks' => $queryData['callbacks']); - - if ($recursive === null && isset($queryData['recursive'])) { - $recursive = $queryData['recursive']; - } - - if ($recursive !== null) { - $modelRecursive = $Model->recursive; - $Model->recursive = $recursive; - } - - if (!empty($queryData['fields'])) { - $noAssocFields = true; - $queryData['fields'] = $this->fields($Model, null, $queryData['fields']); - } else { - $noAssocFields = false; - $queryData['fields'] = $this->fields($Model); - } - - if ($Model->recursive === -1) { - // Primary model data only, no joins. - $associations = array(); - - } else { - $associations = $Model->associations(); - - if ($Model->recursive === 0) { - // Primary model data and its domain. - unset($associations[2], $associations[3]); - } - } - - $originalJoins = $queryData['joins']; - $queryData['joins'] = array(); - - // Generate hasOne and belongsTo associations inside $queryData - $linkedModels = array(); - foreach ($associations as $type) { - if ($type !== 'hasOne' && $type !== 'belongsTo') { - continue; - } - - foreach ($Model->{$type} as $assoc => $assocData) { - $LinkModel = $Model->{$assoc}; - - if ($Model->useDbConfig !== $LinkModel->useDbConfig) { - continue; - } - - if ($noAssocFields) { - $assocData['fields'] = false; - } - - $external = isset($assocData['external']); - - if ($this->generateAssociationQuery($Model, $LinkModel, $type, $assoc, $assocData, $queryData, $external) === true) { - $linkedModels[$type . '/' . $assoc] = true; - } - } - } - - if (!empty($originalJoins)) { - $queryData['joins'] = array_merge($queryData['joins'], $originalJoins); - } - - // Build SQL statement with the primary model, plus hasOne and belongsTo associations - $query = $this->buildAssociationQuery($Model, $queryData); - - $resultSet = $this->fetchAll($query, $Model->cacheQueries); - unset($query); - - if ($resultSet === false) { - $Model->onError(); - return false; - } - - $filtered = array(); - - // Deep associations - if ($Model->recursive > -1) { - $joined = array(); - if (isset($queryData['joins'][0]['alias'])) { - $joined[$Model->alias] = (array)Hash::extract($queryData['joins'], '{n}.alias'); - } - - foreach ($associations as $type) { - foreach ($Model->{$type} as $assoc => $assocData) { - $LinkModel = $Model->{$assoc}; - - if (!isset($linkedModels[$type . '/' . $assoc])) { - $db = $Model->useDbConfig === $LinkModel->useDbConfig ? $this : $LinkModel->getDataSource(); - } elseif ($Model->recursive > 1) { - $db = $this; - } - - if (isset($db) && method_exists($db, 'queryAssociation')) { - $stack = array($assoc); - $stack['_joined'] = $joined; - - $db->queryAssociation($Model, $LinkModel, $type, $assoc, $assocData, $array, true, $resultSet, $Model->recursive - 1, $stack); - unset($db); - - if ($type === 'hasMany' || $type === 'hasAndBelongsToMany') { - $filtered[] = $assoc; - } - } - } - } - } - - if ($queryData['callbacks'] === true || $queryData['callbacks'] === 'after') { - $this->_filterResults($resultSet, $Model, $filtered); - } - - if ($recursive !== null) { - $Model->recursive = $modelRecursive; - } - - return $resultSet; - } - -/** - * Passes association results through afterFind filters of the corresponding model. - * - * The primary model is always excluded, because the filtering is later done by Model::_filterResults(). - * - * @param array &$resultSet Reference of resultset to be filtered. - * @param Model $Model Instance of model to operate against. - * @param array $filtered List of classes already filtered, to be skipped. - * @return array Array of results that have been filtered through $Model->afterFind. - */ - protected function _filterResults(&$resultSet, Model $Model, $filtered = array()) { - if (!is_array($resultSet)) { - return array(); - } - - $current = reset($resultSet); - if (!is_array($current)) { - return array(); - } - - $keys = array_diff(array_keys($current), $filtered, array($Model->alias)); - $filtering = array(); - - foreach ($keys as $className) { - if (!isset($Model->{$className}) || !is_object($Model->{$className})) { - continue; - } - - $LinkedModel = $Model->{$className}; - $filtering[] = $className; - - foreach ($resultSet as $key => &$result) { - $data = $LinkedModel->afterFind(array(array($className => $result[$className])), false); - if (isset($data[0][$className])) { - $result[$className] = $data[0][$className]; - } else { - unset($resultSet[$key]); - } - } - } - - return $filtering; - } - -/** - * Passes association results through afterFind filters of the corresponding model. - * - * Similar to DboSource::_filterResults(), but this filters only specified models. - * The primary model can not be specified, because this call DboSource::_filterResults() internally. - * - * @param array &$resultSet Reference of resultset to be filtered. - * @param Model $Model Instance of model to operate against. - * @param array $toBeFiltered List of classes to be filtered. - * @return array Array of results that have been filtered through $Model->afterFind. - */ - protected function _filterResultsInclusive(&$resultSet, Model $Model, $toBeFiltered = array()) { - $exclude = array(); - - if (is_array($resultSet)) { - $current = reset($resultSet); - if (is_array($current)) { - $exclude = array_diff(array_keys($current), $toBeFiltered); - } - } - - return $this->_filterResults($resultSet, $Model, $exclude); - } - -/** - * Queries associations. - * - * Used to fetch results on recursive models. - * - * - 'hasMany' associations with no limit set: - * Fetch, filter and merge is done recursively for every level. - * - * - 'hasAndBelongsToMany' associations: - * Fetch and filter is done unaffected by the (recursive) level set. - * - * @param Model $Model Primary Model object. - * @param Model $LinkModel Linked model object. - * @param string $type Association type, one of the model association types ie. hasMany. - * @param string $association Association name. - * @param array $assocData Association data. - * @param array &$queryData An array of queryData information containing keys similar to Model::find(). - * @param bool $external Whether or not the association query is on an external datasource. - * @param array &$resultSet Existing results. - * @param int $recursive Number of levels of association. - * @param array $stack A list with joined models. - * @return mixed - * @throws CakeException when results cannot be created. - */ - public function queryAssociation(Model $Model, Model $LinkModel, $type, $association, $assocData, &$queryData, $external, &$resultSet, $recursive, $stack) { - if (isset($stack['_joined'])) { - $joined = $stack['_joined']; - unset($stack['_joined']); - } - - $queryTemplate = $this->generateAssociationQuery($Model, $LinkModel, $type, $association, $assocData, $queryData, $external); - if (empty($queryTemplate)) { - return null; - } - - if (!is_array($resultSet)) { - throw new CakeException(__d('cake_dev', 'Error in Model %s', get_class($Model))); - } - - if ($type === 'hasMany' && empty($assocData['limit']) && !empty($assocData['foreignKey'])) { - // 'hasMany' associations with no limit set. - - $assocIds = array(); - foreach ($resultSet as $result) { - $assocIds[] = $this->insertQueryData('{$__cakeID__$}', $result, $association, $Model, $stack); - } - $assocIds = array_filter($assocIds); - - // Fetch - $assocResultSet = array(); - if (!empty($assocIds)) { - $assocResultSet = $this->_fetchHasMany($Model, $queryTemplate, $assocIds); - } - - // Recursively query associations - if ($recursive > 0 && !empty($assocResultSet) && is_array($assocResultSet)) { - foreach ($LinkModel->associations() as $type1) { - foreach ($LinkModel->{$type1} as $assoc1 => $assocData1) { - $DeepModel = $LinkModel->{$assoc1}; - $tmpStack = $stack; - $tmpStack[] = $assoc1; - - $db = $LinkModel->useDbConfig === $DeepModel->useDbConfig ? $this : $DeepModel->getDataSource(); - - $db->queryAssociation($LinkModel, $DeepModel, $type1, $assoc1, $assocData1, $queryData, true, $assocResultSet, $recursive - 1, $tmpStack); - } - } - } - - // Filter - if ($queryData['callbacks'] === true || $queryData['callbacks'] === 'after') { - $this->_filterResultsInclusive($assocResultSet, $Model, array($association)); - } - - // Merge - return $this->_mergeHasMany($resultSet, $assocResultSet, $association, $Model); - - } elseif ($type === 'hasAndBelongsToMany') { - // 'hasAndBelongsToMany' associations. - - $assocIds = array(); - foreach ($resultSet as $result) { - $assocIds[] = $this->insertQueryData('{$__cakeID__$}', $result, $association, $Model, $stack); - } - $assocIds = array_filter($assocIds); - - // Fetch - $assocResultSet = array(); - if (!empty($assocIds)) { - $assocResultSet = $this->_fetchHasAndBelongsToMany($Model, $queryTemplate, $assocIds, $association); - } - - $habtmAssocData = $Model->hasAndBelongsToMany[$association]; - $foreignKey = $habtmAssocData['foreignKey']; - $joinKeys = array($foreignKey, $habtmAssocData['associationForeignKey']); - list($with, $habtmFields) = $Model->joinModel($habtmAssocData['with'], $joinKeys); - $habtmFieldsCount = count($habtmFields); - - // Filter - if ($queryData['callbacks'] === true || $queryData['callbacks'] === 'after') { - $this->_filterResultsInclusive($assocResultSet, $Model, array($association, $with)); - } - } - - $modelAlias = $Model->alias; - $primaryKey = $Model->primaryKey; - $selfJoin = ($Model->name === $LinkModel->name); - - foreach ($resultSet as &$row) { - if ($type === 'hasOne' || $type === 'belongsTo' || $type === 'hasMany') { - $assocResultSet = array(); - $prefetched = false; - - if (($type === 'hasOne' || $type === 'belongsTo') && - isset($row[$LinkModel->alias], $joined[$Model->alias]) && - in_array($LinkModel->alias, $joined[$Model->alias]) - ) { - $joinedData = Hash::filter($row[$LinkModel->alias]); - if (!empty($joinedData)) { - $assocResultSet[0] = array($LinkModel->alias => $row[$LinkModel->alias]); - } - $prefetched = true; - } else { - $query = $this->insertQueryData($queryTemplate, $row, $association, $Model, $stack); - if ($query !== false) { - $assocResultSet = $this->fetchAll($query, $Model->cacheQueries); - } - } - } - - if (!empty($assocResultSet) && is_array($assocResultSet)) { - if ($recursive > 0) { - foreach ($LinkModel->associations() as $type1) { - foreach ($LinkModel->{$type1} as $assoc1 => $assocData1) { - $DeepModel = $LinkModel->{$assoc1}; - - if ($type1 === 'belongsTo' || - ($type === 'belongsTo' && $DeepModel->alias === $modelAlias) || - ($DeepModel->alias !== $modelAlias) - ) { - $tmpStack = $stack; - $tmpStack[] = $assoc1; - - $db = $LinkModel->useDbConfig === $DeepModel->useDbConfig ? $this : $DeepModel->getDataSource(); - - $db->queryAssociation($LinkModel, $DeepModel, $type1, $assoc1, $assocData1, $queryData, true, $assocResultSet, $recursive - 1, $tmpStack); - } - } - } - } - - if ($type === 'hasAndBelongsToMany') { - $merge = array(); - foreach ($assocResultSet as $data) { - if (isset($data[$with]) && $data[$with][$foreignKey] === $row[$modelAlias][$primaryKey]) { - if ($habtmFieldsCount <= 2) { - unset($data[$with]); - } - $merge[] = $data; - } - } - - if (empty($merge) && !isset($row[$association])) { - $row[$association] = $merge; - } else { - $this->_mergeAssociation($row, $merge, $association, $type); - } - } else { - if (!$prefetched && $LinkModel->useConsistentAfterFind) { - if ($queryData['callbacks'] === true || $queryData['callbacks'] === 'after') { - $this->_filterResultsInclusive($assocResultSet, $Model, array($association)); - } - } - $this->_mergeAssociation($row, $assocResultSet, $association, $type, $selfJoin); - } - - if ($type !== 'hasAndBelongsToMany' && isset($row[$association]) && !$prefetched && !$LinkModel->useConsistentAfterFind) { - $row[$association] = $LinkModel->afterFind($row[$association], false); - } - - } else { - $tempArray[0][$association] = false; - $this->_mergeAssociation($row, $tempArray, $association, $type, $selfJoin); - } - } - } - -/** - * Fetch 'hasMany' associations. - * - * This is just a proxy to maintain BC. - * - * @param Model $Model Primary model object. - * @param string $query Association query template. - * @param array $ids Array of IDs of associated records. - * @return array Association results. - * @see DboSource::_fetchHasMany() - */ - public function fetchAssociated(Model $Model, $query, $ids) { - return $this->_fetchHasMany($Model, $query, $ids); - } - -/** - * Fetch 'hasMany' associations. - * - * @param Model $Model Primary model object. - * @param string $query Association query template. - * @param array $ids Array of IDs of associated records. - * @return array Association results. - */ - protected function _fetchHasMany(Model $Model, $query, $ids) { - $ids = array_unique($ids); - - if (count($ids) > 1) { - $query = str_replace('= ({$__cakeID__$}', 'IN ({$__cakeID__$}', $query); - } - $query = str_replace('{$__cakeID__$}', implode(', ', $ids), $query); - return $this->fetchAll($query, $Model->cacheQueries); - } - -/** - * Fetch 'hasAndBelongsToMany' associations. - * - * @param Model $Model Primary model object. - * @param string $query Association query. - * @param array $ids Array of IDs of associated records. - * @param string $association Association name. - * @return array Association results. - */ - protected function _fetchHasAndBelongsToMany(Model $Model, $query, $ids, $association) { - $ids = array_unique($ids); - - if (count($ids) > 1) { - $query = str_replace('{$__cakeID__$}', '(' . implode(', ', $ids) . ')', $query); - $query = str_replace('= (', 'IN (', $query); - } else { - $query = str_replace('{$__cakeID__$}', $ids[0], $query); - } - $query = str_replace(' WHERE 1 = 1', '', $query); - - return $this->fetchAll($query, $Model->cacheQueries); - } - -/** - * Merge the results of 'hasMany' associations. - * - * Note: this function also deals with the formatting of the data. - * - * @param array &$resultSet Data to merge into. - * @param array $assocResultSet Data to merge. - * @param string $association Name of Model being merged. - * @param Model $Model Model being merged onto. - * @return void - */ - protected function _mergeHasMany(&$resultSet, $assocResultSet, $association, Model $Model) { - $modelAlias = $Model->alias; - $primaryKey = $Model->primaryKey; - $foreignKey = $Model->hasMany[$association]['foreignKey']; - - // Make one pass through children and collect by parent key - // Make second pass through parents and associate children - $mergedByFK = array(); - if (is_array($assocResultSet)) { - foreach ($assocResultSet as $data) { - $fk = $data[$association][$foreignKey]; - if (! array_key_exists($fk, $mergedByFK)) { - $mergedByFK[$fk] = array(); - } - if (count($data) > 1) { - $data = array_merge($data[$association], $data); - unset($data[$association]); - foreach ($data as $key => $name) { - if (is_numeric($key)) { - $data[$association][] = $name; - unset($data[$key]); - } - } - $mergedByFK[$fk][] = $data; - } else { - $mergedByFK[$fk][] = $data[$association]; - } - } - } - - foreach ($resultSet as &$result) { - if (!isset($result[$modelAlias])) { - continue; - } - $merged = array(); - $pk = $result[$modelAlias][$primaryKey]; - if (isset($mergedByFK[$pk])) { - $merged = $mergedByFK[$pk]; - } - $result = Hash::mergeDiff($result, array($association => $merged)); - } - } - -/** - * Merge association of merge into data - * - * @param array &$data The data to merge. - * @param array &$merge The data to merge. - * @param string $association The association name to merge. - * @param string $type The type of association - * @param bool $selfJoin Whether or not this is a self join. - * @return void - */ - protected function _mergeAssociation(&$data, &$merge, $association, $type, $selfJoin = false) { - if (isset($merge[0]) && !isset($merge[0][$association])) { - $association = Inflector::pluralize($association); - } - - $dataAssociation =& $data[$association]; - - if ($type === 'belongsTo' || $type === 'hasOne') { - if (isset($merge[$association])) { - $dataAssociation = $merge[$association][0]; - } else { - if (!empty($merge[0][$association])) { - foreach ($merge[0] as $assoc => $data2) { - if ($assoc !== $association) { - $merge[0][$association][$assoc] = $data2; - } - } - } - if (!isset($dataAssociation)) { - $dataAssociation = array(); - if ($merge[0][$association]) { - $dataAssociation = $merge[0][$association]; - } - } else { - if (is_array($merge[0][$association])) { - $mergeAssocTmp = array(); - foreach ($dataAssociation as $k => $v) { - if (!is_array($v)) { - $dataAssocTmp[$k] = $v; - } - } - - foreach ($merge[0][$association] as $k => $v) { - if (!is_array($v)) { - $mergeAssocTmp[$k] = $v; - } - } - $dataKeys = array_keys($data); - $mergeKeys = array_keys($merge[0]); - - if ($mergeKeys[0] === $dataKeys[0] || $mergeKeys === $dataKeys) { - $dataAssociation[$association] = $merge[0][$association]; - } else { - $diff = Hash::diff($dataAssocTmp, $mergeAssocTmp); - $dataAssociation = array_merge($merge[0][$association], $diff); - } - } elseif ($selfJoin && array_key_exists($association, $merge[0])) { - $dataAssociation = array_merge($dataAssociation, array($association => array())); - } - } - } - } else { - if (isset($merge[0][$association]) && $merge[0][$association] === false) { - if (!isset($dataAssociation)) { - $dataAssociation = array(); - } - } else { - foreach ($merge as $row) { - $insert = array(); - if (count($row) === 1) { - $insert = $row[$association]; - } elseif (isset($row[$association])) { - $insert = array_merge($row[$association], $row); - unset($insert[$association]); - } - - if (empty($dataAssociation) || (isset($dataAssociation) && !in_array($insert, $dataAssociation, true))) { - $dataAssociation[] = $insert; - } - } - } - } - } - -/** - * Prepares fields required by an SQL statement. - * - * When no fields are set, all the $Model fields are returned. - * - * @param Model $Model The model to prepare. - * @param array $queryData An array of queryData information containing keys similar to Model::find(). - * @return array Array containing SQL fields. - */ - public function prepareFields(Model $Model, $queryData) { - if (empty($queryData['fields'])) { - $queryData['fields'] = $this->fields($Model); - - } elseif (!empty($Model->hasMany) && $Model->recursive > -1) { - // hasMany relationships need the $Model primary key. - $assocFields = $this->fields($Model, null, "{$Model->alias}.{$Model->primaryKey}"); - $passedFields = $queryData['fields']; - - if (count($passedFields) > 1 || - (strpos($passedFields[0], $assocFields[0]) === false && !preg_match('/^[a-z]+\(/i', $passedFields[0])) - ) { - $queryData['fields'] = array_merge($passedFields, $assocFields); - } - } - - return array_unique($queryData['fields']); - } - -/** - * Builds an SQL statement. - * - * This is merely a convenient wrapper to DboSource::buildStatement(). - * - * @param Model $Model The model to build an association query for. - * @param array $queryData An array of queryData information containing keys similar to Model::find(). - * @return string String containing an SQL statement. - * @see DboSource::buildStatement() - */ - public function buildAssociationQuery(Model $Model, $queryData) { - $queryData = $this->_scrubQueryData($queryData); - - return $this->buildStatement( - array( - 'fields' => $this->prepareFields($Model, $queryData), - 'table' => $this->fullTableName($Model), - 'alias' => $Model->alias, - 'limit' => $queryData['limit'], - 'offset' => $queryData['offset'], - 'joins' => $queryData['joins'], - 'conditions' => $queryData['conditions'], - 'order' => $queryData['order'], - 'group' => $queryData['group'], - 'having' => $queryData['having'], - 'lock' => $queryData['lock'], - ), - $Model - ); - } - -/** - * Generates a query or part of a query from a single model or two associated models. - * - * Builds a string containing an SQL statement template. - * - * @param Model $Model Primary Model object. - * @param Model|null $LinkModel Linked model object. - * @param string $type Association type, one of the model association types ie. hasMany. - * @param string $association Association name. - * @param array $assocData Association data. - * @param array &$queryData An array of queryData information containing keys similar to Model::find(). - * @param bool $external Whether or not the association query is on an external datasource. - * @return mixed - * String representing a query. - * True, when $external is false and association $type is 'hasOne' or 'belongsTo'. - */ - public function generateAssociationQuery(Model $Model, $LinkModel, $type, $association, $assocData, &$queryData, $external) { - $assocData = $this->_scrubQueryData($assocData); - $queryData = $this->_scrubQueryData($queryData); - - if ($LinkModel === null) { - return $this->buildStatement( - array( - 'fields' => array_unique($queryData['fields']), - 'table' => $this->fullTableName($Model), - 'alias' => $Model->alias, - 'limit' => $queryData['limit'], - 'offset' => $queryData['offset'], - 'joins' => $queryData['joins'], - 'conditions' => $queryData['conditions'], - 'order' => $queryData['order'], - 'group' => $queryData['group'] - ), - $Model - ); - } - - if ($external && !empty($assocData['finderQuery'])) { - return $assocData['finderQuery']; - } - - if ($type === 'hasMany' || $type === 'hasAndBelongsToMany') { - if (empty($assocData['offset']) && !empty($assocData['page'])) { - $assocData['offset'] = ($assocData['page'] - 1) * $assocData['limit']; - } - } - - switch ($type) { - case 'hasOne': - case 'belongsTo': - $conditions = $this->_mergeConditions( - $assocData['conditions'], - $this->getConstraint($type, $Model, $LinkModel, $association, array_merge($assocData, compact('external'))) - ); - - if ($external) { - // Not self join - if ($Model->name !== $LinkModel->name) { - $modelAlias = $Model->alias; - foreach ($conditions as $key => $condition) { - if (is_numeric($key) && strpos($condition, $modelAlias . '.') !== false) { - unset($conditions[$key]); - } - } - } - - $query = array_merge($assocData, array( - 'conditions' => $conditions, - 'table' => $this->fullTableName($LinkModel), - 'fields' => $this->fields($LinkModel, $association, $assocData['fields']), - 'alias' => $association, - 'group' => null - )); - } else { - $join = array( - 'table' => $LinkModel, - 'alias' => $association, - 'type' => isset($assocData['type']) ? $assocData['type'] : 'LEFT', - 'conditions' => trim($this->conditions($conditions, true, false, $Model)) - ); - - $fields = array(); - if ($assocData['fields'] !== false) { - $fields = $this->fields($LinkModel, $association, $assocData['fields']); - } - - $queryData['fields'] = array_merge($this->prepareFields($Model, $queryData), $fields); - - if (!empty($assocData['order'])) { - $queryData['order'][] = $assocData['order']; - } - if (!in_array($join, $queryData['joins'], true)) { - $queryData['joins'][] = $join; - } - - return true; - } - break; - case 'hasMany': - $assocData['fields'] = $this->fields($LinkModel, $association, $assocData['fields']); - if (!empty($assocData['foreignKey'])) { - $assocData['fields'] = array_merge($assocData['fields'], $this->fields($LinkModel, $association, array("{$association}.{$assocData['foreignKey']}"))); - } - - $query = array( - 'conditions' => $this->_mergeConditions($this->getConstraint('hasMany', $Model, $LinkModel, $association, $assocData), $assocData['conditions']), - 'fields' => array_unique($assocData['fields']), - 'table' => $this->fullTableName($LinkModel), - 'alias' => $association, - 'order' => $assocData['order'], - 'limit' => $assocData['limit'], - 'offset' => $assocData['offset'], - 'group' => null - ); - break; - case 'hasAndBelongsToMany': - $joinFields = array(); - $joinAssoc = null; - - if (isset($assocData['with']) && !empty($assocData['with'])) { - $joinKeys = array($assocData['foreignKey'], $assocData['associationForeignKey']); - list($with, $joinFields) = $Model->joinModel($assocData['with'], $joinKeys); - - $joinTbl = $Model->{$with}; - $joinAlias = $joinTbl; - - if (is_array($joinFields) && !empty($joinFields)) { - $joinAssoc = $joinAlias = $joinTbl->alias; - $joinFields = $this->fields($joinTbl, $joinAlias, $joinFields); - } else { - $joinFields = array(); - } - } else { - $joinTbl = $assocData['joinTable']; - $joinAlias = $this->fullTableName($assocData['joinTable']); - } - - $query = array( - 'conditions' => $assocData['conditions'], - 'limit' => $assocData['limit'], - 'offset' => $assocData['offset'], - 'table' => $this->fullTableName($LinkModel), - 'alias' => $association, - 'fields' => array_merge($this->fields($LinkModel, $association, $assocData['fields']), $joinFields), - 'order' => $assocData['order'], - 'group' => null, - 'joins' => array(array( - 'table' => $joinTbl, - 'alias' => $joinAssoc, - 'conditions' => $this->getConstraint('hasAndBelongsToMany', $Model, $LinkModel, $joinAlias, $assocData, $association) - )) - ); - break; - } - - if (isset($query)) { - return $this->buildStatement($query, $Model); - } - - return null; - } - -/** - * Returns a conditions array for the constraint between two models. - * - * @param string $type Association type. - * @param Model $Model Primary Model object. - * @param Model $LinkModel Linked model object. - * @param string $association Association name. - * @param array $assocData Association data. - * @param string $association2 HABTM association name. - * @return array Conditions array defining the constraint between $Model and $LinkModel. - */ - public function getConstraint($type, Model $Model, Model $LinkModel, $association, $assocData, $association2 = null) { - $assocData += array('external' => false); - - if (empty($assocData['foreignKey'])) { - return array(); - } - - switch ($type) { - case 'hasOne': - if ($assocData['external']) { - return array( - "{$association}.{$assocData['foreignKey']}" => '{$__cakeID__$}' - ); - } else { - return array( - "{$association}.{$assocData['foreignKey']}" => $this->identifier("{$Model->alias}.{$Model->primaryKey}") - ); - } - case 'belongsTo': - if ($assocData['external']) { - return array( - "{$association}.{$LinkModel->primaryKey}" => '{$__cakeForeignKey__$}' - ); - } else { - return array( - "{$Model->alias}.{$assocData['foreignKey']}" => $this->identifier("{$association}.{$LinkModel->primaryKey}") - ); - } - case 'hasMany': - return array("{$association}.{$assocData['foreignKey']}" => array('{$__cakeID__$}')); - case 'hasAndBelongsToMany': - return array( - array( - "{$association}.{$assocData['foreignKey']}" => '{$__cakeID__$}' - ), - array( - "{$association}.{$assocData['associationForeignKey']}" => $this->identifier("{$association2}.{$LinkModel->primaryKey}") - ) - ); - } - - return array(); - } - -/** - * Builds and generates a JOIN condition from an array. Handles final clean-up before conversion. - * - * @param array $join An array defining a JOIN condition in a query. - * @return string An SQL JOIN condition to be used in a query. - * @see DboSource::renderJoinStatement() - * @see DboSource::buildStatement() - */ - public function buildJoinStatement($join) { - $data = array_merge(array( - 'type' => null, - 'alias' => null, - 'table' => 'join_table', - 'conditions' => '', - ), $join); - - if (!empty($data['alias'])) { - $data['alias'] = $this->alias . $this->name($data['alias']); - } - if (!empty($data['conditions'])) { - $data['conditions'] = trim($this->conditions($data['conditions'], true, false)); - } - if (!empty($data['table']) && (!is_string($data['table']) || strpos($data['table'], '(') !== 0)) { - $data['table'] = $this->fullTableName($data['table']); - } - return $this->renderJoinStatement($data); - } - -/** - * Builds and generates an SQL statement from an array. Handles final clean-up before conversion. - * - * @param array $query An array defining an SQL query. - * @param Model $Model The model object which initiated the query. - * @return string An executable SQL statement. - * @see DboSource::renderStatement() - */ - public function buildStatement($query, Model $Model) { - $query = array_merge($this->_queryDefaults, $query); - - if (!empty($query['joins'])) { - $count = count($query['joins']); - for ($i = 0; $i < $count; $i++) { - if (is_array($query['joins'][$i])) { - $query['joins'][$i] = $this->buildJoinStatement($query['joins'][$i]); - } - } - } - - return $this->renderStatement('select', array( - 'conditions' => $this->conditions($query['conditions'], true, true, $Model), - 'fields' => implode(', ', $query['fields']), - 'table' => $query['table'], - 'alias' => $this->alias . $this->name($query['alias']), - 'order' => $this->order($query['order'], 'ASC', $Model), - 'limit' => $this->limit($query['limit'], $query['offset']), - 'joins' => implode(' ', $query['joins']), - 'group' => $this->group($query['group'], $Model), - 'having' => $this->having($query['having'], true, $Model), - 'lock' => $this->getLockingHint($query['lock']), - )); - } - -/** - * Renders a final SQL JOIN statement - * - * @param array $data The data to generate a join statement for. - * @return string - */ - public function renderJoinStatement($data) { - if (strtoupper($data['type']) === 'CROSS' || empty($data['conditions'])) { - return "{$data['type']} JOIN {$data['table']} {$data['alias']}"; - } - return trim("{$data['type']} JOIN {$data['table']} {$data['alias']} ON ({$data['conditions']})"); - } - -/** - * Renders a final SQL statement by putting together the component parts in the correct order - * - * @param string $type type of query being run. e.g select, create, update, delete, schema, alter. - * @param array $data Array of data to insert into the query. - * @return string|null Rendered SQL expression to be run, otherwise null. - */ - public function renderStatement($type, $data) { - extract($data); - $aliases = null; - - switch (strtolower($type)) { - case 'select': - $having = !empty($having) ? " $having" : ''; - $lock = !empty($lock) ? " $lock" : ''; - return trim("SELECT {$fields} FROM {$table} {$alias} {$joins} {$conditions} {$group}{$having} {$order} {$limit}{$lock}"); - case 'create': - return "INSERT INTO {$table} ({$fields}) VALUES ({$values})"; - case 'update': - if (!empty($alias)) { - $aliases = "{$this->alias}{$alias} {$joins} "; - } - return trim("UPDATE {$table} {$aliases}SET {$fields} {$conditions}"); - case 'delete': - if (!empty($alias)) { - $aliases = "{$this->alias}{$alias} {$joins} "; - } - return trim("DELETE {$alias} FROM {$table} {$aliases}{$conditions}"); - case 'schema': - foreach (array('columns', 'indexes', 'tableParameters') as $var) { - if (is_array(${$var})) { - ${$var} = "\t" . implode(",\n\t", array_filter(${$var})); - } else { - ${$var} = ''; - } - } - if (trim($indexes) !== '') { - $columns .= ','; - } - return "CREATE TABLE {$table} (\n{$columns}{$indexes}) {$tableParameters};"; - case 'alter': - return null; - } - } - -/** - * Merges a mixed set of string/array conditions. - * - * @param mixed $query The query to merge conditions for. - * @param mixed $assoc The association names. - * @return array - */ - protected function _mergeConditions($query, $assoc) { - if (empty($assoc)) { - return $query; - } - - if (is_array($query)) { - return array_merge((array)$assoc, $query); - } - - if (!empty($query)) { - $query = array($query); - if (is_array($assoc)) { - $query = array_merge($query, $assoc); - } else { - $query[] = $assoc; - } - return $query; - } - - return $assoc; - } - -/** - * Generates and executes an SQL UPDATE statement for given model, fields, and values. - * For databases that do not support aliases in UPDATE queries. - * - * @param Model $Model The model to update. - * @param array $fields The fields to update - * @param array $values The values fo the fields. - * @param mixed $conditions The conditions for the update. When non-empty $values will not be quoted. - * @return bool Success - */ - public function update(Model $Model, $fields = array(), $values = null, $conditions = null) { - if (!$values) { - $combined = $fields; - } else { - $combined = array_combine($fields, $values); - } - - $fields = implode(', ', $this->_prepareUpdateFields($Model, $combined, empty($conditions))); - - $alias = $joins = null; - $table = $this->fullTableName($Model); - $conditions = $this->_matchRecords($Model, $conditions); - - if ($conditions === false) { - return false; - } - $query = compact('table', 'alias', 'joins', 'fields', 'conditions'); - - if (!$this->execute($this->renderStatement('update', $query))) { - $Model->onError(); - return false; - } - return true; - } - -/** - * Quotes and prepares fields and values for an SQL UPDATE statement - * - * @param Model $Model The model to prepare fields for. - * @param array $fields The fields to update. - * @param bool $quoteValues If values should be quoted, or treated as SQL snippets - * @param bool $alias Include the model alias in the field name - * @return array Fields and values, quoted and prepared - */ - protected function _prepareUpdateFields(Model $Model, $fields, $quoteValues = true, $alias = false) { - $quotedAlias = $this->startQuote . $Model->alias . $this->endQuote; - $schema = $Model->schema(); - - $updates = array(); - foreach ($fields as $field => $value) { - if ($alias && strpos($field, '.') === false) { - $quoted = $Model->escapeField($field); - } elseif (!$alias && strpos($field, '.') !== false) { - $quoted = $this->name(str_replace($quotedAlias . '.', '', str_replace( - $Model->alias . '.', '', $field - ))); - } else { - $quoted = $this->name($field); - } - - if ($value === null) { - $updates[] = $quoted . ' = NULL'; - continue; - } - $update = $quoted . ' = '; - - if ($quoteValues) { - $update .= $this->value($value, $Model->getColumnType($field), isset($schema[$field]['null']) ? $schema[$field]['null'] : true); - } elseif ($Model->getColumnType($field) === 'boolean' && (is_int($value) || is_bool($value))) { - $update .= $this->boolean($value, true); - } elseif (!$alias) { - $update .= str_replace($quotedAlias . '.', '', str_replace( - $Model->alias . '.', '', $value - )); - } else { - $update .= $value; - } - $updates[] = $update; - } - return $updates; - } - -/** - * Generates and executes an SQL DELETE statement. - * For databases that do not support aliases in UPDATE queries. - * - * @param Model $Model The model to delete from - * @param mixed $conditions The conditions to use. If empty the model's primary key will be used. - * @return bool Success - */ - public function delete(Model $Model, $conditions = null) { - $alias = $joins = null; - $table = $this->fullTableName($Model); - $conditions = $this->_matchRecords($Model, $conditions); - - if ($conditions === false) { - return false; - } - - if ($this->execute($this->renderStatement('delete', compact('alias', 'table', 'joins', 'conditions'))) === false) { - $Model->onError(); - return false; - } - return true; - } - -/** - * Gets a list of record IDs for the given conditions. Used for multi-record updates and deletes - * in databases that do not support aliases in UPDATE/DELETE queries. - * - * @param Model $Model The model to find matching records for. - * @param mixed $conditions The conditions to match against. - * @return array List of record IDs - */ - protected function _matchRecords(Model $Model, $conditions = null) { - if ($conditions === true) { - $conditions = $this->conditions(true); - } elseif ($conditions === null) { - $conditions = $this->conditions($this->defaultConditions($Model, $conditions, false), true, true, $Model); - } else { - $noJoin = true; - foreach ($conditions as $field => $value) { - $originalField = $field; - if (strpos($field, '.') !== false) { - list(, $field) = explode('.', $field); - $field = ltrim($field, $this->startQuote); - $field = rtrim($field, $this->endQuote); - } - if (!$Model->hasField($field)) { - $noJoin = false; - break; - } - if ($field !== $originalField) { - $conditions[$field] = $value; - unset($conditions[$originalField]); - } - } - if ($noJoin === true) { - return $this->conditions($conditions); - } - $idList = $Model->find('all', array( - 'fields' => "{$Model->alias}.{$Model->primaryKey}", - 'conditions' => $conditions - )); - - if (empty($idList)) { - return false; - } - - $conditions = $this->conditions(array( - $Model->primaryKey => Hash::extract($idList, "{n}.{$Model->alias}.{$Model->primaryKey}") - )); - } - - return $conditions; - } - -/** - * Returns an array of SQL JOIN conditions from a model's associations. - * - * @param Model $Model The model to get joins for.2 - * @return array - */ - protected function _getJoins(Model $Model) { - $join = array(); - $joins = array_merge($Model->getAssociated('hasOne'), $Model->getAssociated('belongsTo')); - - foreach ($joins as $assoc) { - if (!isset($Model->{$assoc})) { - continue; - } - - $LinkModel = $Model->{$assoc}; - - if ($Model->useDbConfig !== $LinkModel->useDbConfig) { - continue; - } - - $assocData = $Model->getAssociated($assoc); - - $join[] = $this->buildJoinStatement(array( - 'table' => $LinkModel, - 'alias' => $assoc, - 'type' => isset($assocData['type']) ? $assocData['type'] : 'LEFT', - 'conditions' => trim($this->conditions( - $this->_mergeConditions($assocData['conditions'], $this->getConstraint($assocData['association'], $Model, $LinkModel, $assoc, $assocData)), - true, - false, - $Model - )) - )); - } - - return $join; - } - -/** - * Returns an SQL calculation, i.e. COUNT() or MAX() - * - * @param Model $Model The model to get a calculated field for. - * @param string $func Lowercase name of SQL function, i.e. 'count' or 'max' - * @param array $params Function parameters (any values must be quoted manually) - * @return string An SQL calculation function - */ - public function calculate(Model $Model, $func, $params = array()) { - $params = (array)$params; - - switch (strtolower($func)) { - case 'count': - if (!isset($params[0])) { - $params[0] = '*'; - } - if (!isset($params[1])) { - $params[1] = 'count'; - } - if ($Model->isVirtualField($params[0])) { - $arg = $this->_quoteFields($Model->getVirtualField($params[0])); - } else { - $arg = $this->name($params[0]); - } - return 'COUNT(' . $arg . ') AS ' . $this->name($params[1]); - case 'max': - case 'min': - if (!isset($params[1])) { - $params[1] = $params[0]; - } - if ($Model->isVirtualField($params[0])) { - $arg = $this->_quoteFields($Model->getVirtualField($params[0])); - } else { - $arg = $this->name($params[0]); - } - return strtoupper($func) . '(' . $arg . ') AS ' . $this->name($params[1]); - } - } - -/** - * Deletes all the records in a table and resets the count of the auto-incrementing - * primary key, where applicable. - * - * @param Model|string $table A string or model class representing the table to be truncated - * @return bool SQL TRUNCATE TABLE statement, false if not applicable. - */ - public function truncate($table) { - return $this->execute('TRUNCATE TABLE ' . $this->fullTableName($table)); - } - -/** - * Check if the server support nested transactions - * - * @return bool - */ - public function nestedTransactionSupported() { - return false; - } - -/** - * Begin a transaction - * - * @return bool True on success, false on fail - * (i.e. if the database/model does not support transactions, - * or a transaction has not started). - */ - public function begin() { - if ($this->_transactionStarted) { - if ($this->nestedTransactionSupported()) { - return $this->_beginNested(); - } - $this->_transactionNesting++; - return $this->_transactionStarted; - } - - $this->_transactionNesting = 0; - if ($this->fullDebug) { - $this->took = $this->numRows = $this->affected = false; - $this->logQuery('BEGIN'); - } - return $this->_transactionStarted = $this->_connection->beginTransaction(); - } - -/** - * Begin a nested transaction - * - * @return bool - */ - protected function _beginNested() { - $query = 'SAVEPOINT LEVEL' . ++$this->_transactionNesting; - if ($this->fullDebug) { - $this->took = $this->numRows = $this->affected = false; - $this->logQuery($query); - } - $this->_connection->exec($query); - return true; - } - -/** - * Commit a transaction - * - * @return bool True on success, false on fail - * (i.e. if the database/model does not support transactions, - * or a transaction has not started). - */ - public function commit() { - if (!$this->_transactionStarted) { - return false; - } - - if ($this->_transactionNesting === 0) { - if ($this->fullDebug) { - $this->took = $this->numRows = $this->affected = false; - $this->logQuery('COMMIT'); - } - $this->_transactionStarted = false; - return $this->_connection->commit(); - } - - if ($this->nestedTransactionSupported()) { - return $this->_commitNested(); - } - - $this->_transactionNesting--; - return true; - } - -/** - * Commit a nested transaction - * - * @return bool - */ - protected function _commitNested() { - $query = 'RELEASE SAVEPOINT LEVEL' . $this->_transactionNesting--; - if ($this->fullDebug) { - $this->took = $this->numRows = $this->affected = false; - $this->logQuery($query); - } - $this->_connection->exec($query); - return true; - } - -/** - * Rollback a transaction - * - * @return bool True on success, false on fail - * (i.e. if the database/model does not support transactions, - * or a transaction has not started). - */ - public function rollback() { - if (!$this->_transactionStarted) { - return false; - } - - if ($this->_transactionNesting === 0) { - if ($this->fullDebug) { - $this->took = $this->numRows = $this->affected = false; - $this->logQuery('ROLLBACK'); - } - $this->_transactionStarted = false; - return $this->_connection->rollBack(); - } - - if ($this->nestedTransactionSupported()) { - return $this->_rollbackNested(); - } - - $this->_transactionNesting--; - return true; - } - -/** - * Rollback a nested transaction - * - * @return bool - */ - protected function _rollbackNested() { - $query = 'ROLLBACK TO SAVEPOINT LEVEL' . $this->_transactionNesting--; - if ($this->fullDebug) { - $this->took = $this->numRows = $this->affected = false; - $this->logQuery($query); - } - $this->_connection->exec($query); - return true; - } - -/** - * Returns the ID generated from the previous INSERT operation. - * - * @param mixed $source The source to get an id for. - * @return mixed - */ - public function lastInsertId($source = null) { - return $this->_connection->lastInsertId(); - } - -/** - * Creates a default set of conditions from the model if $conditions is null/empty. - * If conditions are supplied then they will be returned. If a model doesn't exist and no conditions - * were provided either null or false will be returned based on what was input. - * - * @param Model $Model The model to get conditions for. - * @param string|array|bool $conditions Array of conditions, conditions string, null or false. If an array of conditions, - * or string conditions those conditions will be returned. With other values the model's existence will be checked. - * If the model doesn't exist a null or false will be returned depending on the input value. - * @param bool $useAlias Use model aliases rather than table names when generating conditions - * @return mixed Either null, false, $conditions or an array of default conditions to use. - * @see DboSource::update() - * @see DboSource::conditions() - */ - public function defaultConditions(Model $Model, $conditions, $useAlias = true) { - if (!empty($conditions)) { - return $conditions; - } - $exists = $Model->exists($Model->getID()); - if (!$exists && ($conditions !== null || !empty($Model->__safeUpdateMode))) { - return false; - } elseif (!$exists) { - return null; - } - $alias = $Model->alias; - - if (!$useAlias) { - $alias = $this->fullTableName($Model, false); - } - return array("{$alias}.{$Model->primaryKey}" => $Model->getID()); - } - -/** - * Returns a key formatted like a string Model.fieldname(i.e. Post.title, or Country.name) - * - * @param Model $Model The model to get a key for. - * @param string $key The key field. - * @param string $assoc The association name. - * @return string - */ - public function resolveKey(Model $Model, $key, $assoc = null) { - if (strpos('.', $key) !== false) { - return $this->name($Model->alias) . '.' . $this->name($key); - } - return $key; - } - -/** - * Private helper method to remove query metadata in given data array. - * - * @param array $data The data to scrub. - * @return array - */ - protected function _scrubQueryData($data) { - static $base = null; - if ($base === null) { - $base = array_fill_keys(array('conditions', 'fields', 'joins', 'order', 'limit', 'offset', 'group'), array()); - $base['having'] = null; - $base['lock'] = null; - $base['callbacks'] = null; - } - return (array)$data + $base; - } - -/** - * Converts model virtual fields into sql expressions to be fetched later - * - * @param Model $Model The model to get virtual fields for. - * @param string $alias Alias table name - * @param array $fields virtual fields to be used on query - * @return array - */ - protected function _constructVirtualFields(Model $Model, $alias, $fields) { - $virtual = array(); - foreach ($fields as $field) { - $virtualField = $this->name($alias . $this->virtualFieldSeparator . $field); - $virtualFieldExpression = $Model->getVirtualField($field); - if (is_object($virtualFieldExpression) && $virtualFieldExpression->type == 'expression') { - $expression = $virtualFieldExpression->value; - } else { - $expression = $this->_quoteFields($virtualFieldExpression); - } - $virtual[] = '(' . $expression . ") {$this->alias} {$virtualField}"; - } - return $virtual; - } - -/** - * Generates the fields list of an SQL query. - * - * @param Model $Model The model to get fields for. - * @param string $alias Alias table name - * @param mixed $fields The provided list of fields. - * @param bool $quote If false, returns fields array unquoted - * @return array - */ - public function fields(Model $Model, $alias = null, $fields = array(), $quote = true) { - if (empty($alias)) { - $alias = $Model->alias; - } - $virtualFields = $Model->getVirtualField(); - $cacheKey = array( - $alias, - get_class($Model), - $Model->alias, - $virtualFields, - $fields, - $quote, - ConnectionManager::getSourceName($this), - $Model->schemaName, - $Model->table - ); - $cacheKey = $this->cacheMethodHasher(serialize($cacheKey)); - if ($return = $this->cacheMethod(__FUNCTION__, $cacheKey)) { - return $return; - } - $allFields = empty($fields); - if ($allFields) { - $fields = array_keys($Model->schema()); - } elseif (!is_array($fields)) { - $fields = CakeText::tokenize($fields); - } - $fields = array_values(array_filter($fields)); - $allFields = $allFields || in_array('*', $fields) || in_array($Model->alias . '.*', $fields); - - $virtual = array(); - if (!empty($virtualFields)) { - $virtualKeys = array_keys($virtualFields); - foreach ($virtualKeys as $field) { - $virtualKeys[] = $Model->alias . '.' . $field; - } - $virtual = ($allFields) ? $virtualKeys : array_intersect($virtualKeys, $fields); - foreach ($virtual as $i => $field) { - if (strpos($field, '.') !== false) { - $virtual[$i] = str_replace($Model->alias . '.', '', $field); - } - $fields = array_diff($fields, array($field)); - } - $fields = array_values($fields); - } - if (!$quote) { - if (!empty($virtual)) { - $fields = array_merge($fields, $this->_constructVirtualFields($Model, $alias, $virtual)); - } - return $fields; - } - $count = count($fields); - - if ($count >= 1 && !in_array($fields[0], array('*', 'COUNT(*)'))) { - for ($i = 0; $i < $count; $i++) { - if (is_string($fields[$i]) && in_array($fields[$i], $virtual)) { - unset($fields[$i]); - continue; - } - if (is_object($fields[$i]) && isset($fields[$i]->type) && $fields[$i]->type === 'expression') { - $fields[$i] = $fields[$i]->value; - } elseif (preg_match('/^\(.*\)\s' . $this->alias . '.*/i', $fields[$i])) { - continue; - } elseif (!preg_match('/^.+\\(.*\\)/', $fields[$i])) { - $prepend = ''; - - if (strpos($fields[$i], 'DISTINCT') !== false) { - $prepend = 'DISTINCT '; - $fields[$i] = trim(str_replace('DISTINCT', '', $fields[$i])); - } - $dot = strpos($fields[$i], '.'); - - if ($dot === false) { - $prefix = !( - strpos($fields[$i], ' ') !== false || - strpos($fields[$i], '(') !== false - ); - $fields[$i] = $this->name(($prefix ? $alias . '.' : '') . $fields[$i]); - } else { - if (strpos($fields[$i], ',') === false) { - $build = explode('.', $fields[$i]); - if (!Hash::numeric($build)) { - $fields[$i] = $this->name(implode('.', $build)); - } - } - } - $fields[$i] = $prepend . $fields[$i]; - } elseif (preg_match('/\(([\.\w]+)\)/', $fields[$i], $field)) { - if (isset($field[1])) { - if (strpos($field[1], '.') === false) { - $field[1] = $this->name($alias . '.' . $field[1]); - } else { - $field[0] = explode('.', $field[1]); - if (!Hash::numeric($field[0])) { - $field[0] = implode('.', array_map(array(&$this, 'name'), $field[0])); - $fields[$i] = preg_replace('/\(' . $field[1] . '\)/', '(' . $field[0] . ')', $fields[$i], 1); - } - } - } - } - } - } - if (!empty($virtual)) { - $fields = array_merge($fields, $this->_constructVirtualFields($Model, $alias, $virtual)); - } - return $this->cacheMethod(__FUNCTION__, $cacheKey, array_unique($fields)); - } - -/** - * Creates a WHERE clause by parsing given conditions data. If an array or string - * conditions are provided those conditions will be parsed and quoted. If a boolean - * is given it will be integer cast as condition. Null will return 1 = 1. - * - * Results of this method are stored in a memory cache. This improves performance, but - * because the method uses a hashing algorithm it can have collisions. - * Setting DboSource::$cacheMethods to false will disable the memory cache. - * - * @param mixed $conditions Array or string of conditions, or any value. - * @param bool $quoteValues If true, values should be quoted - * @param bool $where If true, "WHERE " will be prepended to the return value - * @param Model $Model A reference to the Model instance making the query - * @return string SQL fragment - */ - public function conditions($conditions, $quoteValues = true, $where = true, Model $Model = null) { - $clause = $out = ''; - - if ($where) { - $clause = ' WHERE '; - } - - if (is_array($conditions) && !empty($conditions)) { - $out = $this->conditionKeysToString($conditions, $quoteValues, $Model); - - if (empty($out)) { - return $clause . ' 1 = 1'; - } - return $clause . implode(' AND ', $out); - } - - if (is_bool($conditions)) { - return $clause . (int)$conditions . ' = 1'; - } - - if (empty($conditions) || trim($conditions) === '') { - return $clause . '1 = 1'; - } - - $clauses = '/^WHERE\\x20|^GROUP\\x20BY\\x20|^HAVING\\x20|^ORDER\\x20BY\\x20/i'; - - if (preg_match($clauses, $conditions)) { - $clause = ''; - } - - $conditions = $this->_quoteFields($conditions); - - return $clause . $conditions; - } - -/** - * Creates a WHERE clause by parsing given conditions array. Used by DboSource::conditions(). - * - * @param array $conditions Array or string of conditions - * @param bool $quoteValues If true, values should be quoted - * @param Model $Model A reference to the Model instance making the query - * @return string SQL fragment - */ - public function conditionKeysToString($conditions, $quoteValues = true, Model $Model = null) { - $out = array(); - $data = $columnType = null; - - foreach ($conditions as $key => $value) { - $join = ' AND '; - $not = null; - - if (is_array($value)) { - $valueInsert = ( - !empty($value) && - (substr_count($key, '?') === count($value) || substr_count($key, ':') === count($value)) - ); - } - - if (is_numeric($key) && empty($value)) { - continue; - } elseif (is_numeric($key) && is_string($value)) { - $out[] = $this->_quoteFields($value); - } elseif ((is_numeric($key) && is_array($value)) || in_array(strtolower(trim($key)), $this->_sqlBoolOps)) { - if (in_array(strtolower(trim($key)), $this->_sqlBoolOps)) { - $join = ' ' . strtoupper($key) . ' '; - } else { - $key = $join; - } - $value = $this->conditionKeysToString($value, $quoteValues, $Model); - - if (strpos($join, 'NOT') !== false) { - if (strtoupper(trim($key)) === 'NOT') { - $key = 'AND ' . trim($key); - } - $not = 'NOT '; - } - - if (empty($value)) { - continue; - } - - if (empty($value[1])) { - if ($not) { - $out[] = $not . '(' . $value[0] . ')'; - } else { - $out[] = $value[0]; - } - } else { - $out[] = '(' . $not . '(' . implode(') ' . strtoupper($key) . ' (', $value) . '))'; - } - } else { - if (is_object($value) && isset($value->type)) { - if ($value->type === 'identifier') { - $data .= $this->name($key) . ' = ' . $this->name($value->value); - } elseif ($value->type === 'expression') { - if (is_numeric($key)) { - $data .= $value->value; - } else { - $data .= $this->name($key) . ' = ' . $value->value; - } - } - } elseif (is_array($value) && !empty($value) && !$valueInsert) { - $keys = array_keys($value); - if ($keys === array_values($keys)) { - if (count($value) === 1 && !preg_match('/\s+(?:NOT|IN|\!=)$/', $key)) { - $data = $this->_quoteFields($key) . ' = ('; - if ($quoteValues) { - if ($Model !== null) { - $columnType = $Model->getColumnType($key); - } - $data .= implode(', ', $this->value($value, $columnType)); - } - $data .= ')'; - } else { - $data = $this->_parseKey($key, $value, $Model); - } - } else { - $ret = $this->conditionKeysToString($value, $quoteValues, $Model); - if (count($ret) > 1) { - $data = '(' . implode(') AND (', $ret) . ')'; - } elseif (isset($ret[0])) { - $data = $ret[0]; - } - } - } elseif (is_numeric($key) && !empty($value)) { - $data = $this->_quoteFields($value); - } else { - $data = $this->_parseKey(trim($key), $value, $Model); - } - - if ($data) { - $out[] = $data; - $data = null; - } - } - } - return $out; - } - -/** - * Extracts a Model.field identifier and an SQL condition operator from a string, formats - * and inserts values, and composes them into an SQL snippet. - * - * @param string $key An SQL key snippet containing a field and optional SQL operator - * @param mixed $value The value(s) to be inserted in the string - * @param Model $Model Model object initiating the query - * @return string - */ - protected function _parseKey($key, $value, Model $Model = null) { - $operatorMatch = '/^(((' . implode(')|(', $this->_sqlOps); - $operatorMatch .= ')\\x20?)|<[>=]?(?![^>]+>)\\x20?|[>=!]{1,3}(?!<)\\x20?)/is'; - $bound = (strpos($key, '?') !== false || (is_array($value) && strpos($key, ':') !== false)); - - $key = trim($key); - if (strpos($key, ' ') === false) { - $operator = '='; - } else { - list($key, $operator) = explode(' ', $key, 2); - - if (!preg_match($operatorMatch, trim($operator)) && strpos($operator, ' ') !== false) { - $key = $key . ' ' . $operator; - $split = strrpos($key, ' '); - $operator = substr($key, $split); - $key = substr($key, 0, $split); - } - } - - $virtual = false; - $type = null; - - if ($Model !== null) { - if ($Model->isVirtualField($key)) { - $virtualField = $Model->getVirtualField($key); - if (is_object($virtualField) && $virtualField->type == 'expression') { - $key = $virtualField->value; - } else { - $key = $this->_quoteFields($virtualField); - } - $virtual = true; - } - - $type = $Model->getColumnType($key); - } - - $null = $value === null || (is_array($value) && empty($value)); - - if (strtolower($operator) === 'not') { - $data = $this->conditionKeysToString( - array($operator => array($key => $value)), true, $Model - ); - return $data[0]; - } - - $value = $this->value($value, $type); - - if (!$virtual && $key !== '?') { - $isKey = ( - strpos($key, '(') !== false || - strpos($key, ')') !== false || - strpos($key, '|') !== false || - strpos($key, '->') !== false - ); - $key = $isKey ? $this->_quoteFields($key) : $this->name($key); - } - - if ($bound) { - return CakeText::insert($key . ' ' . trim($operator), $value); - } - - if (!preg_match($operatorMatch, trim($operator))) { - $operator .= is_array($value) ? ' IN' : ' ='; - } - $operator = trim($operator); - - if (is_array($value)) { - $value = implode(', ', $value); - - switch ($operator) { - case '=': - $operator = 'IN'; - break; - case '!=': - case '<>': - $operator = 'NOT IN'; - break; - } - $value = "({$value})"; - } elseif ($null || $value === 'NULL') { - switch ($operator) { - case '=': - $operator = 'IS'; - break; - case '!=': - case '<>': - $operator = 'IS NOT'; - break; - } - } - if ($virtual) { - return "({$key}) {$operator} {$value}"; - } - return "{$key} {$operator} {$value}"; - } - -/** - * Quotes Model.fields - * - * @param string $conditions The conditions to quote. - * @return string or false if no match - */ - protected function _quoteFields($conditions) { - $start = $end = null; - $original = $conditions; - - if (!empty($this->startQuote)) { - $start = preg_quote($this->startQuote); - } - if (!empty($this->endQuote)) { - $end = preg_quote($this->endQuote); - } - - // Remove quotes and requote all the Model.field names. - $conditions = str_replace(array($start, $end), '', $conditions); - $conditions = preg_replace_callback( - '/(?:[\'\"][^\'\"\\\]*(?:\\\.[^\'\"\\\]*)*[\'\"])|([a-z0-9_][a-z0-9\\-_]*\\.[a-z0-9_][a-z0-9_\\-]*[a-z0-9_])|([a-z0-9_][a-z0-9_\\-]*)(?=->)/i', - array(&$this, '_quoteMatchedField'), - $conditions - ); - // Quote `table_name AS Alias` - $conditions = preg_replace( - '/(\s[a-z0-9\\-_.' . $start . $end . ']*' . $end . ')\s+AS\s+([a-z0-9\\-_]+)/i', - '\1 AS ' . $this->startQuote . '\2' . $this->endQuote, - $conditions - ); - if ($conditions !== null) { - return $conditions; - } - return $original; - } - -/** - * Auxiliary function to quote matches `Model.fields` from a preg_replace_callback call - * - * @param string $match matched string - * @return string quoted string - */ - protected function _quoteMatchedField($match) { - if (is_numeric($match[0])) { - return $match[0]; - } - return $this->name($match[0]); - } - -/** - * Returns a limit statement in the correct format for the particular database. - * - * @param int $limit Limit of results returned - * @param int $offset Offset from which to start results - * @return string SQL limit/offset statement - */ - public function limit($limit, $offset = null) { - if ($limit) { - $rt = ' LIMIT'; - - if ($offset) { - $rt .= sprintf(' %u,', $offset); - } - - $rt .= sprintf(' %u', $limit); - return $rt; - } - return null; - } - -/** - * Returns an ORDER BY clause as a string. - * - * @param array|string $keys Field reference, as a key (i.e. Post.title) - * @param string $direction Direction (ASC or DESC) - * @param Model $Model Model reference (used to look for virtual field) - * @return string ORDER BY clause - */ - public function order($keys, $direction = 'ASC', Model $Model = null) { - if (!is_array($keys)) { - $keys = array($keys); - } - $keys = array_filter($keys); - - $result = array(); - while (!empty($keys)) { - $key = key($keys); - $dir = current($keys); - array_shift($keys); - - if (is_numeric($key)) { - $key = $dir; - $dir = $direction; - } - - if (is_string($key) && strpos($key, ',') !== false && !preg_match('/\(.+\,.+\)/', $key)) { - $key = array_map('trim', explode(',', $key)); - } - - if (is_array($key)) { - //Flatten the array - $key = array_reverse($key, true); - foreach ($key as $k => $v) { - if (is_numeric($k)) { - array_unshift($keys, $v); - } else { - $keys = array($k => $v) + $keys; - } - } - continue; - } elseif (is_object($key) && isset($key->type) && $key->type === 'expression') { - $result[] = $key->value; - continue; - } - - if (preg_match('/\\x20(ASC|DESC).*/i', $key, $_dir)) { - $dir = $_dir[0]; - $key = preg_replace('/\\x20(ASC|DESC).*/i', '', $key); - } - - $key = trim($key); - - if ($Model !== null) { - if ($Model->isVirtualField($key)) { - $key = '(' . $this->_quoteFields($Model->getVirtualField($key)) . ')'; - } - - list($alias) = pluginSplit($key); - - if ($alias !== $Model->alias && is_object($Model->{$alias}) && $Model->{$alias}->isVirtualField($key)) { - $key = '(' . $this->_quoteFields($Model->{$alias}->getVirtualField($key)) . ')'; - } - } - - if (strpos($key, '.')) { - $key = preg_replace_callback('/([a-zA-Z0-9_-]{1,})\\.([a-zA-Z0-9_-]{1,})/', array(&$this, '_quoteMatchedField'), $key); - } - - if (!preg_match('/\s/', $key) && strpos($key, '.') === false) { - $key = $this->name($key); - } - - $key .= ' ' . trim($dir); - - $result[] = $key; - } - - if (!empty($result)) { - return ' ORDER BY ' . implode(', ', $result); - } - - return ''; - } - -/** - * Create a GROUP BY SQL clause. - * - * @param string|array $fields Group By fields - * @param Model $Model The model to get group by fields for. - * @return string Group By clause or null. - */ - public function group($fields, Model $Model = null) { - if (empty($fields)) { - return null; - } - - if (!is_array($fields)) { - $fields = array($fields); - } - - if ($Model !== null) { - foreach ($fields as $index => $key) { - if ($Model->isVirtualField($key)) { - $fields[$index] = '(' . $Model->getVirtualField($key) . ')'; - } - } - } - - $fields = implode(', ', $fields); - - return ' GROUP BY ' . $this->_quoteFields($fields); - } - -/** - * Create a HAVING SQL clause. - * - * @param mixed $fields Array or string of conditions - * @param bool $quoteValues If true, values should be quoted - * @param Model $Model A reference to the Model instance making the query - * @return string|null HAVING clause or null - */ - public function having($fields, $quoteValues = true, Model $Model = null) { - if (!$fields) { - return null; - } - return ' HAVING ' . $this->conditions($fields, $quoteValues, false, $Model); - } - -/** - * Returns a locking hint for the given mode. - * - * Currently, this method only returns FOR UPDATE when the mode is set to true. - * - * @param mixed $mode Lock mode - * @return string|null FOR UPDATE clause or null - */ - public function getLockingHint($mode) { - if ($mode !== true) { - return null; - } - return ' FOR UPDATE'; - } - -/** - * Disconnects database, kills the connection and says the connection is closed. - * - * @return void - */ - public function close() { - $this->disconnect(); - } - -/** - * Checks if the specified table contains any record matching specified SQL - * - * @param Model $Model Model to search - * @param string $sql SQL WHERE clause (condition only, not the "WHERE" part) - * @return bool True if the table has a matching record, else false - */ - public function hasAny(Model $Model, $sql) { - $sql = $this->conditions($sql); - $table = $this->fullTableName($Model); - $alias = $this->alias . $this->name($Model->alias); - $where = $sql ? "{$sql}" : ' WHERE 1 = 1'; - $id = $Model->escapeField(); - - $out = $this->fetchRow("SELECT COUNT({$id}) {$this->alias}count FROM {$table} {$alias}{$where}"); - - if (is_array($out)) { - return $out[0]['count']; - } - return false; - } - -/** - * Gets the length of a database-native column description, or null if no length - * - * @param string $real Real database-layer column type (i.e. "varchar(255)") - * @return mixed An integer or string representing the length of the column, or null for unknown length. - */ - public function length($real) { - preg_match('/([\w\s]+)(?:\((.+?)\))?(\sunsigned)?/i', $real, $result); - $types = array( - 'int' => 1, 'tinyint' => 1, 'smallint' => 1, 'mediumint' => 1, 'integer' => 1, 'bigint' => 1 - ); - - $type = $length = null; - if (isset($result[1])) { - $type = $result[1]; - } - if (isset($result[2])) { - $length = $result[2]; - } - $sign = isset($result[3]); - - $isFloat = in_array($type, array('dec', 'decimal', 'float', 'numeric', 'double')); - if ($isFloat && strpos($length, ',') !== false) { - return $length; - } - - if ($length === null) { - return null; - } - - if (isset($types[$type])) { - return (int)$length; - } - if (in_array($type, array('enum', 'set'))) { - return null; - } - return (int)$length; - } - -/** - * Translates between PHP boolean values and Database (faked) boolean values - * - * @param mixed $data Value to be translated - * @param bool $quote Whether or not the field should be cast to a string. - * @return string|bool Converted boolean value - */ - public function boolean($data, $quote = false) { - if ($quote) { - return !empty($data) ? '1' : '0'; - } - return !empty($data); - } - -/** - * Inserts multiple values into a table - * - * @param string $table The table being inserted into. - * @param array $fields The array of field/column names being inserted. - * @param array $values The array of values to insert. The values should - * be an array of rows. Each row should have values keyed by the column name. - * Each row must have the values in the same order as $fields. - * @return bool - */ - public function insertMulti($table, $fields, $values) { - $table = $this->fullTableName($table); - $holder = implode(',', array_fill(0, count($fields), '?')); - $fields = implode(', ', array_map(array(&$this, 'name'), $fields)); - - $pdoMap = array( - 'integer' => PDO::PARAM_INT, - 'float' => PDO::PARAM_STR, - 'boolean' => PDO::PARAM_BOOL, - 'string' => PDO::PARAM_STR, - 'text' => PDO::PARAM_STR - ); - $columnMap = array(); - - $sql = "INSERT INTO {$table} ({$fields}) VALUES ({$holder})"; - $statement = $this->_connection->prepare($sql); - $this->begin(); - - foreach ($values[key($values)] as $key => $val) { - $type = $this->introspectType($val); - $columnMap[$key] = $pdoMap[$type]; - } - - foreach ($values as $value) { - $i = 1; - foreach ($value as $col => $val) { - $statement->bindValue($i, $val, $columnMap[$col]); - $i += 1; - } - $t = microtime(true); - $statement->execute(); - $statement->closeCursor(); - - if ($this->fullDebug) { - $this->took = round((microtime(true) - $t) * 1000, 0); - $this->numRows = $this->affected = $statement->rowCount(); - $this->logQuery($sql, $value); - } - } - return $this->commit(); - } - -/** - * Reset a sequence based on the MAX() value of $column. Useful - * for resetting sequences after using insertMulti(). - * - * This method should be implemented by datasources that require sequences to be used. - * - * @param string $table The name of the table to update. - * @param string $column The column to use when resetting the sequence value. - * @return bool Success. - */ - public function resetSequence($table, $column) { - } - -/** - * Returns an array of the indexes in given datasource name. - * - * @param string $model Name of model to inspect - * @return array Fields in table. Keys are column and unique - */ - public function index($model) { - return array(); - } - -/** - * Generate a database-native schema for the given Schema object - * - * @param CakeSchema $schema An instance of a subclass of CakeSchema - * @param string $tableName Optional. If specified only the table name given will be generated. - * Otherwise, all tables defined in the schema are generated. - * @return string - */ - public function createSchema($schema, $tableName = null) { - if (!$schema instanceof CakeSchema) { - trigger_error(__d('cake_dev', 'Invalid schema object'), E_USER_WARNING); - return null; - } - $out = ''; - - foreach ($schema->tables as $curTable => $columns) { - if (!$tableName || $tableName === $curTable) { - $cols = $indexes = $tableParameters = array(); - $primary = null; - $table = $this->fullTableName($curTable); - - $primaryCount = 0; - foreach ($columns as $col) { - if (isset($col['key']) && $col['key'] === 'primary') { - $primaryCount++; - } - } - - foreach ($columns as $name => $col) { - if (is_string($col)) { - $col = array('type' => $col); - } - $isPrimary = isset($col['key']) && $col['key'] === 'primary'; - // Multi-column primary keys are not supported. - if ($isPrimary && $primaryCount > 1) { - unset($col['key']); - $isPrimary = false; - } - if ($isPrimary) { - $primary = $name; - } - if ($name !== 'indexes' && $name !== 'tableParameters') { - $col['name'] = $name; - if (!isset($col['type'])) { - $col['type'] = 'string'; - } - $cols[] = $this->buildColumn($col); - } elseif ($name === 'indexes') { - $indexes = array_merge($indexes, $this->buildIndex($col, $table)); - } elseif ($name === 'tableParameters') { - $tableParameters = array_merge($tableParameters, $this->buildTableParameters($col, $table)); - } - } - if (!isset($columns['indexes']['PRIMARY']) && !empty($primary)) { - $col = array('PRIMARY' => array('column' => $primary, 'unique' => 1)); - $indexes = array_merge($indexes, $this->buildIndex($col, $table)); - } - $columns = $cols; - $out .= $this->renderStatement('schema', compact('table', 'columns', 'indexes', 'tableParameters')) . "\n\n"; - } - } - return $out; - } - -/** - * Generate an alter syntax from CakeSchema::compare() - * - * @param mixed $compare The comparison data. - * @param string $table The table name. - * @return bool - */ - public function alterSchema($compare, $table = null) { - return false; - } - -/** - * Generate a "drop table" statement for the given Schema object - * - * @param CakeSchema $schema An instance of a subclass of CakeSchema - * @param string $table Optional. If specified only the table name given will be generated. - * Otherwise, all tables defined in the schema are generated. - * @return string - */ - public function dropSchema(CakeSchema $schema, $table = null) { - $out = ''; - - if ($table && array_key_exists($table, $schema->tables)) { - return $this->_dropTable($table) . "\n"; - } elseif ($table) { - return $out; - } - - foreach (array_keys($schema->tables) as $curTable) { - $out .= $this->_dropTable($curTable) . "\n"; - } - return $out; - } - -/** - * Generate a "drop table" statement for a single table - * - * @param type $table Name of the table to drop - * @return string Drop table SQL statement - */ - protected function _dropTable($table) { - return 'DROP TABLE ' . $this->fullTableName($table) . ";"; - } - -/** - * Generate a database-native column schema string - * - * @param array $column An array structured like the following: array('name' => 'value', 'type' => 'value'[, options]), - * where options can be 'default', 'length', or 'key'. - * @return string - */ - public function buildColumn($column) { - $name = $type = null; - extract(array_merge(array('null' => true), $column)); - - if (empty($name) || empty($type)) { - trigger_error(__d('cake_dev', 'Column name or type not defined in schema'), E_USER_WARNING); - return null; - } - - if (!isset($this->columns[$type]) && substr($type, 0, 4) !== 'enum') { - trigger_error(__d('cake_dev', 'Column type %s does not exist', $type), E_USER_WARNING); - return null; - } - - if (substr($type, 0, 4) === 'enum') { - $out = $this->name($name) . ' ' . $type; - } else { - $real = $this->columns[$type]; - $out = $this->name($name) . ' ' . $real['name']; - if (isset($column['length'])) { - $length = $column['length']; - } elseif (isset($column['limit'])) { - $length = $column['limit']; - } elseif (isset($real['length'])) { - $length = $real['length']; - } elseif (isset($real['limit'])) { - $length = $real['limit']; - } - if (isset($length)) { - $out .= '(' . $length . ')'; - } - } - - if (($column['type'] === 'integer' || $column['type'] === 'float') && isset($column['default']) && $column['default'] === '') { - $column['default'] = null; - } - $out = $this->_buildFieldParameters($out, $column, 'beforeDefault'); - - if (isset($column['key']) && $column['key'] === 'primary' && ($type === 'integer' || $type === 'biginteger')) { - $out .= ' ' . $this->columns['primary_key']['name']; - } elseif (isset($column['key']) && $column['key'] === 'primary') { - $out .= ' NOT NULL'; - } elseif (isset($column['default']) && isset($column['null']) && $column['null'] === false) { - $out .= ' DEFAULT ' . $this->value($column['default'], $type) . ' NOT NULL'; - } elseif (isset($column['default'])) { - $out .= ' DEFAULT ' . $this->value($column['default'], $type); - } elseif ($type !== 'timestamp' && !empty($column['null'])) { - $out .= ' DEFAULT NULL'; - } elseif ($type === 'timestamp' && !empty($column['null'])) { - $out .= ' NULL'; - } elseif (isset($column['null']) && $column['null'] === false) { - $out .= ' NOT NULL'; - } - if (in_array($type, array('timestamp', 'datetime')) && isset($column['default']) && strtolower($column['default']) === 'current_timestamp') { - $out = str_replace(array("'CURRENT_TIMESTAMP'", "'current_timestamp'"), 'CURRENT_TIMESTAMP', $out); - } - return $this->_buildFieldParameters($out, $column, 'afterDefault'); - } - -/** - * Build the field parameters, in a position - * - * @param string $columnString The partially built column string - * @param array $columnData The array of column data. - * @param string $position The position type to use. 'beforeDefault' or 'afterDefault' are common - * @return string a built column with the field parameters added. - */ - protected function _buildFieldParameters($columnString, $columnData, $position) { - foreach ($this->fieldParameters as $paramName => $value) { - if (isset($columnData[$paramName]) && $value['position'] == $position) { - if (isset($value['options']) && !in_array($columnData[$paramName], $value['options'], true)) { - continue; - } - if (isset($value['types']) && !in_array($columnData['type'], $value['types'], true)) { - continue; - } - $val = $columnData[$paramName]; - if ($value['quote']) { - $val = $this->value($val); - } - $columnString .= ' ' . $value['value'] . (empty($value['noVal']) ? $value['join'] . $val : ''); - } - } - return $columnString; - } - -/** - * Format indexes for create table. - * - * @param array $indexes The indexes to build - * @param string $table The table name. - * @return array - */ - public function buildIndex($indexes, $table = null) { - $join = array(); - foreach ($indexes as $name => $value) { - $out = ''; - if ($name === 'PRIMARY') { - $out .= 'PRIMARY '; - $name = null; - } else { - if (!empty($value['unique'])) { - $out .= 'UNIQUE '; - } - $name = $this->startQuote . $name . $this->endQuote; - } - if (is_array($value['column'])) { - $out .= 'KEY ' . $name . ' (' . implode(', ', array_map(array(&$this, 'name'), $value['column'])) . ')'; - } else { - $out .= 'KEY ' . $name . ' (' . $this->name($value['column']) . ')'; - } - $join[] = $out; - } - return $join; - } - -/** - * Read additional table parameters - * - * @param string $name The table name to read. - * @return array - */ - public function readTableParameters($name) { - $parameters = array(); - if (method_exists($this, 'listDetailedSources')) { - $currentTableDetails = $this->listDetailedSources($name); - foreach ($this->tableParameters as $paramName => $parameter) { - if (!empty($parameter['column']) && !empty($currentTableDetails[$parameter['column']])) { - $parameters[$paramName] = $currentTableDetails[$parameter['column']]; - } - } - } - return $parameters; - } - -/** - * Format parameters for create table - * - * @param array $parameters The parameters to create SQL for. - * @param string $table The table name. - * @return array - */ - public function buildTableParameters($parameters, $table = null) { - $result = array(); - foreach ($parameters as $name => $value) { - if (isset($this->tableParameters[$name])) { - if ($this->tableParameters[$name]['quote']) { - $value = $this->value($value); - } - $result[] = $this->tableParameters[$name]['value'] . $this->tableParameters[$name]['join'] . $value; - } - } - return $result; - } - -/** - * Guesses the data type of an array - * - * @param string $value The value to introspect for type data. - * @return string - */ - public function introspectType($value) { - if (!is_array($value)) { - if (is_bool($value)) { - return 'boolean'; - } - if (is_float($value) && (float)$value === $value) { - return 'float'; - } - if (is_int($value) && (int)$value === $value) { - return 'integer'; - } - if (is_string($value) && strlen($value) > 255) { - return 'text'; - } - return 'string'; - } - - $isAllFloat = $isAllInt = true; - $containsInt = $containsString = false; - foreach ($value as $valElement) { - $valElement = trim($valElement); - if (!is_float($valElement) && !preg_match('/^[\d]+\.[\d]+$/', $valElement)) { - $isAllFloat = false; - } else { - continue; - } - if (!is_int($valElement) && !preg_match('/^[\d]+$/', $valElement)) { - $isAllInt = false; - } else { - $containsInt = true; - continue; - } - $containsString = true; - } - - if ($isAllFloat) { - return 'float'; - } - if ($isAllInt) { - return 'integer'; - } - - if ($containsInt && !$containsString) { - return 'integer'; - } - return 'string'; - } - -/** - * Empties the query caches. - * - * @return void - */ - public function flushQueryCache() { - $this->_queryCache = array(); - } - -/** - * Writes a new key for the in memory sql query cache - * - * @param string $sql SQL query - * @param mixed $data result of $sql query - * @param array $params query params bound as values - * @return void - */ - protected function _writeQueryCache($sql, $data, $params = array()) { - if (preg_match('/^\s*select/i', $sql)) { - $this->_queryCache[$sql][serialize($params)] = $data; - } - } - -/** - * Returns the result for a sql query if it is already cached - * - * @param string $sql SQL query - * @param array $params query params bound as values - * @return mixed results for query if it is cached, false otherwise - */ - public function getQueryCache($sql, $params = array()) { - if (isset($this->_queryCache[$sql]) && preg_match('/^\s*select/i', $sql)) { - $serialized = serialize($params); - if (isset($this->_queryCache[$sql][$serialized])) { - return $this->_queryCache[$sql][$serialized]; - } - } - return false; - } - -/** - * Used for storing in cache the results of the in-memory methodCache - */ - public function __destruct() { - if ($this->_methodCacheChange) { - Cache::write('method_cache', static::$methodCache, '_cake_core_'); - } - parent::__destruct(); - } +class DboSource extends DataSource +{ + + /** + * Caches result from query parsing operations. Cached results for both DboSource::name() and DboSource::fields() + * will be stored here. + * + * Method caching uses `md5` (by default) to construct cache keys. If you have problems with collisions, + * try a different hashing algorithm by overriding DboSource::cacheMethodHasher or set DboSource::$cacheMethods to false. + * + * @var array + */ + public static $methodCache = []; + /** + * Description string for this Database Data Source. + * + * @var string + */ + public $description = "Database Data Source"; + /** + * index definition, standard cake, primary, index, unique + * + * @var array + */ + public $index = ['PRI' => 'primary', 'MUL' => 'index', 'UNI' => 'unique']; + /** + * Database keyword used to assign aliases to identifiers. + * + * @var string + */ + public $alias = 'AS '; + /** + * Whether or not to cache the results of DboSource::name() and DboSource::fields() into the memory cache. + * Set to false to disable the use of the memory cache. + * + * @var bool + */ + public $cacheMethods = true; + + /** + * Flag to support nested transactions. If it is set to false, you will be able to use + * the transaction methods (begin/commit/rollback), but just the global transaction will + * be executed. + * + * @var bool + */ + public $useNestedTransactions = false; + + /** + * Print full query debug info? + * + * @var bool + */ + public $fullDebug = false; + + /** + * String to hold how many rows were affected by the last SQL operation. + * + * @var string + */ + public $affected = null; + + /** + * Number of rows in current resultset + * + * @var int + */ + public $numRows = null; + + /** + * Time the last query took + * + * @var int + */ + public $took = null; + /** + * The DataSource configuration key name + * + * @var string + */ + public $configKeyName = null; + /** + * The starting character that this DataSource uses for quoted identifiers. + * + * @var string + */ + public $startQuote = null; + /** + * The ending character that this DataSource uses for quoted identifiers. + * + * @var string + */ + public $endQuote = null; + /** + * Separator string for virtualField composition + * + * @var string + */ + public $virtualFieldSeparator = '__'; + /** + * List of table engine specific parameters used on table creating + * + * @var array + */ + public $tableParameters = []; + /** + * List of engine specific additional field parameters used on table creating + * + * @var array + */ + public $fieldParameters = []; + /** + * Map of the columns contained in a result. + * + * @var array + */ + public $map = []; + /** + * Result + * + * @var array|PDOStatement + */ + protected $_result = null; + /** + * Queries count. + * + * @var int + */ + protected $_queriesCnt = 0; + /** + * Total duration of all queries. + * + * @var int + */ + protected $_queriesTime = null; + /** + * Log of queries executed by this DataSource + * + * @var array + */ + protected $_queriesLog = []; + /** + * Maximum number of items in query log + * + * This is to prevent query log taking over too much memory. + * + * @var int + */ + protected $_queriesLogMax = 200; + /** + * Caches serialized results of executed queries + * + * @var array + */ + protected $_queryCache = []; + /** + * A reference to the physical connection of this DataSource + * + * @var array + */ + protected $_connection = null; + /** + * The set of valid SQL operations usable in a WHERE statement + * + * @var array + */ + protected $_sqlOps = ['like', 'ilike', 'rlike', 'or', 'not', 'in', 'between', 'regexp', 'similar to']; + /** + * The set of valid SQL boolean operations usable in a WHERE statement + * + * @var array + */ + protected $_sqlBoolOps = ['and', 'or', 'not', 'and not', 'or not', 'xor', '||', '&&']; + /** + * Indicates the level of nested transactions + * + * @var int + */ + protected $_transactionNesting = 0; + /** + * Default fields that are used by the DBO + * + * @var array + */ + protected $_queryDefaults = [ + 'conditions' => [], + 'fields' => null, + 'table' => null, + 'alias' => null, + 'order' => null, + 'limit' => null, + 'joins' => [], + 'group' => null, + 'offset' => null, + 'having' => null, + 'lock' => null, + ]; + /** + * Indicates whether there was a change on the cached results on the methods of this class + * This will be used for storing in a more persistent cache + * + * @var bool + */ + protected $_methodCacheChange = false; + + /** + * Constructor + * + * @param array $config Array of configuration information for the Datasource. + * @param bool $autoConnect Whether or not the datasource should automatically connect. + * @throws MissingConnectionException when a connection cannot be made. + */ + public function __construct($config = null, $autoConnect = true) + { + if (!isset($config['prefix'])) { + $config['prefix'] = ''; + } + parent::__construct($config); + $this->fullDebug = Configure::read('debug') > 1; + if (!$this->enabled()) { + throw new MissingConnectionException([ + 'class' => get_class($this), + 'message' => __d('cake_dev', 'Selected driver is not enabled'), + 'enabled' => false + ]); + } + if ($autoConnect) { + $this->connect(); + } + } + + /** + * Connects to the database. + * + * @return bool + */ + public function connect() + { + // This method is implemented in subclasses + return $this->connected; + } + + /** + * Reconnects to database server with optional new settings + * + * @param array $config An array defining the new configuration settings + * @return bool True on success, false on failure + */ + public function reconnect($config = []) + { + $this->disconnect(); + $this->setConfig($config); + $this->_sources = null; + + return $this->connect(); + } + + /** + * Disconnects from database. + * + * @return bool Always true + */ + public function disconnect() + { + if ($this->_result instanceof PDOStatement) { + $this->_result->closeCursor(); + } + $this->_connection = null; + $this->connected = false; + return true; + } + + /** + * Get the underlying connection object. + * + * @return PDO + */ + public function getConnection() + { + return $this->_connection; + } + + /** + * Gets the version string of the database server + * + * @return string The database version + */ + public function getVersion() + { + return $this->_connection->getAttribute(PDO::ATTR_SERVER_VERSION); + } + + /** + * Returns an object to represent a database expression in a query. Expression objects + * are not sanitized or escaped. + * + * @param string $expression An arbitrary SQL expression to be inserted into a query. + * @return stdClass An object representing a database expression to be used in a query + */ + public function expression($expression) + { + $obj = new stdClass(); + $obj->type = 'expression'; + $obj->value = $expression; + return $obj; + } + + /** + * Executes given SQL statement. + * + * @param string $sql SQL statement + * @param array $params Additional options for the query. + * @return mixed Resource or object representing the result set, or false on failure + */ + public function rawQuery($sql, $params = []) + { + $this->took = $this->numRows = false; + return $this->execute($sql, [], $params); + } + + /** + * Queries the database with given SQL statement, and obtains some metadata about the result + * (rows affected, timing, any errors, number of rows in resultset). The query is also logged. + * If Configure::read('debug') is set, the log is shown all the time, else it is only shown on errors. + * + * ### Options + * + * - log - Whether or not the query should be logged to the memory log. + * + * @param string $sql SQL statement + * @param array $options The options for executing the query. + * @param array $params values to be bound to the query. + * @return mixed Resource or object representing the result set, or false on failure + */ + public function execute($sql, $options = [], $params = []) + { + $options += ['log' => $this->fullDebug]; + + $t = microtime(true); + $this->_result = $this->_execute($sql, $params); + + if ($options['log']) { + $this->took = round((microtime(true) - $t) * 1000, 0); + $this->numRows = $this->affected = $this->lastAffected(); + $this->logQuery($sql, $params); + } + + return $this->_result; + } + + /** + * Executes given SQL statement. + * + * @param string $sql SQL statement + * @param array $params list of params to be bound to query + * @param array $prepareOptions Options to be used in the prepare statement + * @return mixed PDOStatement if query executes with no problem, true as the result of a successful, false on error + * query returning no rows, such as a CREATE statement, false otherwise + * @throws PDOException + */ + protected function _execute($sql, $params = [], $prepareOptions = []) + { + $sql = trim($sql); + if (preg_match('/^(?:CREATE|ALTER|DROP)\s+(?:TABLE|INDEX)/i', $sql)) { + $statements = array_filter(explode(';', $sql)); + if (count($statements) > 1) { + $result = array_map([$this, '_execute'], $statements); + return array_search(false, $result) === false; + } + } + + try { + $query = $this->_connection->prepare($sql, $prepareOptions); + $query->setFetchMode(PDO::FETCH_LAZY); + if (!$query->execute($params)) { + $this->_result = $query; + $query->closeCursor(); + return false; + } + if (!$query->columnCount()) { + $query->closeCursor(); + if (!$query->rowCount()) { + return true; + } + } + return $query; + } catch (PDOException $e) { + if (isset($query->queryString)) { + $e->queryString = $query->queryString; + } else { + $e->queryString = $sql; + } + throw $e; + } + } + + /** + * Returns number of affected rows in previous database operation. If no previous operation exists, + * this returns false. + * + * @param mixed $source The source to check. + * @return int Number of affected rows + */ + public function lastAffected($source = null) + { + if ($this->hasResult()) { + return $this->_result->rowCount(); + } + return 0; + } + + /** + * Checks if the result is valid + * + * @return bool True if the result is valid else false + */ + public function hasResult() + { + return $this->_result instanceof PDOStatement; + } + + /** + * Log given SQL query. + * + * @param string $sql SQL statement + * @param array $params Values binded to the query (prepared statements) + * @return void + */ + public function logQuery($sql, $params = []) + { + $this->_queriesCnt++; + $this->_queriesTime += $this->took; + $this->_queriesLog[] = [ + 'query' => $sql, + 'params' => $params, + 'affected' => $this->affected, + 'numRows' => $this->numRows, + 'took' => $this->took + ]; + if (count($this->_queriesLog) > $this->_queriesLogMax) { + array_shift($this->_queriesLog); + } + } + + /** + * Returns a formatted error message from previous database operation. + * + * @param PDOStatement $query the query to extract the error from if any + * @return string Error message with error number + */ + public function lastError(PDOStatement $query = null) + { + if ($query) { + $error = $query->errorInfo(); + } else { + $error = $this->_connection->errorInfo(); + } + if (empty($error[2])) { + return null; + } + return $error[1] . ': ' . $error[2]; + } + + /** + * Returns number of rows in previous resultset. If no previous resultset exists, + * this returns false. + * + * @param mixed $source Not used + * @return int Number of rows in resultset + */ + public function lastNumRows($source = null) + { + return $this->lastAffected(); + } + + /** + * DataSource Query abstraction + * + * @return resource Result resource identifier. + */ + public function query() + { + $args = func_get_args(); + $fields = null; + $order = null; + $limit = null; + $page = null; + $recursive = null; + + if (count($args) === 1) { + return $this->fetchAll($args[0]); + } else if (count($args) > 1 && preg_match('/^find(\w*)By(.+)/', $args[0], $matches)) { + $params = $args[1]; + + $findType = lcfirst($matches[1]); + $field = Inflector::underscore($matches[2]); + + $or = (strpos($field, '_or_') !== false); + if ($or) { + $field = explode('_or_', $field); + } else { + $field = explode('_and_', $field); + } + $off = count($field) - 1; + + if (isset($params[1 + $off])) { + $fields = $params[1 + $off]; + } + + if (isset($params[2 + $off])) { + $order = $params[2 + $off]; + } + + if (!array_key_exists(0, $params)) { + return false; + } + + $c = 0; + $conditions = []; + + foreach ($field as $f) { + $conditions[$args[2]->alias . '.' . $f] = $params[$c++]; + } + + if ($or) { + $conditions = ['OR' => $conditions]; + } + + if ($findType !== 'first' && $findType !== '') { + if (isset($params[3 + $off])) { + $limit = $params[3 + $off]; + } + + if (isset($params[4 + $off])) { + $page = $params[4 + $off]; + } + + if (isset($params[5 + $off])) { + $recursive = $params[5 + $off]; + } + return $args[2]->find($findType, compact('conditions', 'fields', 'order', 'limit', 'page', 'recursive')); + } + if (isset($params[3 + $off])) { + $recursive = $params[3 + $off]; + } + return $args[2]->find('first', compact('conditions', 'fields', 'order', 'recursive')); + } + if (isset($args[1]) && $args[1] === true) { + return $this->fetchAll($args[0], true); + } else if (isset($args[1]) && !is_array($args[1])) { + return $this->fetchAll($args[0], false); + } else if (isset($args[1]) && is_array($args[1])) { + if (isset($args[2])) { + $cache = $args[2]; + } else { + $cache = true; + } + return $this->fetchAll($args[0], $args[1], ['cache' => $cache]); + } + } + + /** + * Returns an array of all result rows for a given SQL query. + * + * Returns false if no rows matched. + * + * ### Options + * + * - `cache` - Returns the cached version of the query, if exists and stores the result in cache. + * This is a non-persistent cache, and only lasts for a single request. This option + * defaults to true. If you are directly calling this method, you can disable caching + * by setting $options to `false` + * + * @param string $sql SQL statement + * @param array|bool $params Either parameters to be bound as values for the SQL statement, + * or a boolean to control query caching. + * @param array $options additional options for the query. + * @return bool|array Array of resultset rows, or false if no rows matched + */ + public function fetchAll($sql, $params = [], $options = []) + { + if (is_string($options)) { + $options = ['modelName' => $options]; + } + if (is_bool($params)) { + $options['cache'] = $params; + $params = []; + } + $options += ['cache' => true]; + $cache = $options['cache']; + if ($cache && ($cached = $this->getQueryCache($sql, $params)) !== false) { + return $cached; + } + $result = $this->execute($sql, [], $params); + if ($result) { + $out = []; + + if ($this->hasResult()) { + $first = $this->fetchRow(); + if ($first) { + $out[] = $first; + } + while ($item = $this->fetchResult()) { + if (isset($item[0])) { + $this->fetchVirtualField($item); + } + $out[] = $item; + } + } + + if (!is_bool($result) && $cache) { + $this->_writeQueryCache($sql, $out, $params); + } + + if (empty($out) && is_bool($this->_result)) { + return $this->_result; + } + return $out; + } + return false; + } + + /** + * Returns the result for a sql query if it is already cached + * + * @param string $sql SQL query + * @param array $params query params bound as values + * @return mixed results for query if it is cached, false otherwise + */ + public function getQueryCache($sql, $params = []) + { + if (isset($this->_queryCache[$sql]) && preg_match('/^\s*select/i', $sql)) { + $serialized = serialize($params); + if (isset($this->_queryCache[$sql][$serialized])) { + return $this->_queryCache[$sql][$serialized]; + } + } + return false; + } + + /** + * Returns a row from current resultset as an array + * + * @param string $sql Some SQL to be executed. + * @return array The fetched row as an array + */ + public function fetchRow($sql = null) + { + if (is_string($sql) && strlen($sql) > 5 && !$this->execute($sql)) { + return null; + } + + if ($this->hasResult()) { + $this->resultSet($this->_result); + $resultRow = $this->fetchResult(); + if (isset($resultRow[0])) { + $this->fetchVirtualField($resultRow); + } + return $resultRow; + } + return null; + } + + /** + * Builds a map of the columns contained in a result + * + * @param PDOStatement $results The results to format. + * @return void + */ + public function resultSet($results) + { + // This method is implemented in subclasses + } + + /** + * Fetches the next row from the current result set + * + * @return bool + */ + public function fetchResult() + { + return false; + } + + /** + * Modifies $result array to place virtual fields in model entry where they belongs to + * + * @param array &$result Reference to the fetched row + * @return void + */ + public function fetchVirtualField(&$result) + { + if (isset($result[0]) && is_array($result[0])) { + foreach ($result[0] as $field => $value) { + if (strpos($field, $this->virtualFieldSeparator) === false) { + continue; + } + + list($alias, $virtual) = explode($this->virtualFieldSeparator, $field); + + if (!ClassRegistry::isKeySet($alias)) { + return; + } + + $Model = ClassRegistry::getObject($alias); + + if ($Model->isVirtualField($virtual)) { + $result[$alias][$virtual] = $value; + unset($result[0][$field]); + } + } + if (empty($result[0])) { + unset($result[0]); + } + } + } + + /** + * Writes a new key for the in memory sql query cache + * + * @param string $sql SQL query + * @param mixed $data result of $sql query + * @param array $params query params bound as values + * @return void + */ + protected function _writeQueryCache($sql, $data, $params = []) + { + if (preg_match('/^\s*select/i', $sql)) { + $this->_queryCache[$sql][serialize($params)] = $data; + } + } + + /** + * Returns a single field of the first of query results for a given SQL query, or false if empty. + * + * @param string $name The name of the field to get. + * @param string $sql The SQL query. + * @return mixed Value of field read, or false if not found. + */ + public function field($name, $sql) + { + $data = $this->fetchRow($sql); + if (empty($data[$name])) { + return false; + } + return $data[$name]; + } + + /** + * Empties the method caches. + * These caches are used by DboSource::name() and DboSource::conditions() + * + * @return void + */ + public function flushMethodCache() + { + $this->_methodCacheChange = true; + static::$methodCache = []; + } + + /** + * Checks if the source is connected to the database. + * + * @return bool True if the database is connected, else false + */ + public function isConnected() + { + if ($this->_connection === null) { + $connected = false; + } else { + try { + $connected = $this->_connection->query('SELECT 1'); + } catch (Exception $e) { + $connected = false; + } + } + $this->connected = !empty($connected); + return $this->connected; + } + + /** + * Outputs the contents of the queries log. If in a non-CLI environment the sql_log element + * will be rendered and output. If in a CLI environment, a plain text log is generated. + * + * @param bool $sorted Get the queries sorted by time taken, defaults to false. + * @return void + */ + public function showLog($sorted = false) + { + $log = $this->getLog($sorted, false); + if (empty($log['log'])) { + return; + } + if (PHP_SAPI !== 'cli') { + $controller = null; + $View = new View($controller, false); + $View->set('sqlLogs', [$this->configKeyName => $log]); + echo $View->element('sql_dump', ['_forced_from_dbo_' => true]); + } else { + foreach ($log['log'] as $k => $i) { + print (($k + 1) . ". {$i['query']}\n"); + } + } + } + + /** + * Get the query log as an array. + * + * @param bool $sorted Get the queries sorted by time taken, defaults to false. + * @param bool $clear If True the existing log will cleared. + * @return array Array of queries run as an array + */ + public function getLog($sorted = false, $clear = true) + { + if ($sorted) { + $log = sortByKey($this->_queriesLog, 'took', 'desc', SORT_NUMERIC); + } else { + $log = $this->_queriesLog; + } + if ($clear) { + $this->_queriesLog = []; + } + return ['log' => $log, 'count' => $this->_queriesCnt, 'time' => $this->_queriesTime]; + } + + /** + * The "C" in CRUD + * + * Creates new records in the database. + * + * @param Model $Model Model object that the record is for. + * @param array $fields An array of field names to insert. If null, $Model->data will be + * used to generate field names. + * @param array $values An array of values with keys matching the fields. If null, $Model->data will + * be used to generate values. + * @return bool Success + */ + public function create(Model $Model, $fields = null, $values = null) + { + $id = null; + + if (!$fields) { + unset($fields, $values); + $fields = array_keys($Model->data); + $values = array_values($Model->data); + } + $count = count($fields); + + for ($i = 0; $i < $count; $i++) { + $schema = $Model->schema(); + $valueInsert[] = $this->value($values[$i], $Model->getColumnType($fields[$i]), isset($schema[$fields[$i]]['null']) ? $schema[$fields[$i]]['null'] : true); + $fieldInsert[] = $this->name($fields[$i]); + if ($fields[$i] === $Model->primaryKey) { + $id = $values[$i]; + } + } + + $query = [ + 'table' => $this->fullTableName($Model), + 'fields' => implode(', ', $fieldInsert), + 'values' => implode(', ', $valueInsert) + ]; + + if ($this->execute($this->renderStatement('create', $query))) { + if (empty($id)) { + $id = $this->lastInsertId($this->fullTableName($Model, false, false), $Model->primaryKey); + } + $Model->setInsertID($id); + $Model->id = $id; + return true; + } + + $Model->onError(); + return false; + } + + /** + * Returns a quoted and escaped string of $data for use in an SQL statement. + * + * @param string $data String to be prepared for use in an SQL statement + * @param string $column The column datatype into which this data will be inserted. + * @param bool $null Column allows NULL values + * @return string Quoted and escaped data + */ + public function value($data, $column = null, $null = true) + { + if (is_array($data) && !empty($data)) { + return array_map( + [&$this, 'value'], + $data, array_fill(0, count($data), $column) + ); + } else if (is_object($data) && isset($data->type, $data->value)) { + if ($data->type === 'identifier') { + return $this->name($data->value); + } else if ($data->type === 'expression') { + return $data->value; + } + } else if (in_array($data, ['{$__cakeID__$}', '{$__cakeForeignKey__$}'], true)) { + return $data; + } + + if ($data === null || (is_array($data) && empty($data))) { + return 'NULL'; + } + + if (empty($column)) { + $column = $this->introspectType($data); + } + + $isStringEnum = false; + if (strpos($column, "enum") === 0) { + $firstValue = null; + if (preg_match("/(enum\()(.*)(\))/i", $column, $acceptingValues)) { + $values = explode(",", $acceptingValues[2]); + $firstValue = $values[0]; + } + if (is_string($firstValue)) { + $isStringEnum = true; + } + } + + switch ($column) { + case 'binary': + return $this->_connection->quote($data, PDO::PARAM_LOB); + case 'boolean': + return $this->_connection->quote($this->boolean($data, true), PDO::PARAM_BOOL); + case 'string': + case 'text': + return $this->_connection->quote($data, PDO::PARAM_STR); + default: + if ($data === '') { + return $null ? 'NULL' : '""'; + } + if (is_float($data)) { + return str_replace(',', '.', strval($data)); + } + if (((is_int($data) || $data === '0') || ( + is_numeric($data) && + strpos($data, ',') === false && + $data[0] != '0' && + strpos($data, 'e') === false) + ) && !$isStringEnum + ) { + return $data; + } + return $this->_connection->quote($data); + } + } + + /** + * Returns a quoted name of $data for use in an SQL statement. + * Strips fields out of SQL functions before quoting. + * + * Results of this method are stored in a memory cache. This improves performance, but + * because the method uses a hashing algorithm it can have collisions. + * Setting DboSource::$cacheMethods to false will disable the memory cache. + * + * @param mixed $data Either a string with a column to quote. An array of columns to quote or an + * object from DboSource::expression() or DboSource::identifier() + * @return string SQL field + */ + public function name($data) + { + if (is_object($data) && isset($data->type)) { + return $data->value; + } + if ($data === '*') { + return '*'; + } + if (is_array($data)) { + foreach ($data as $i => $dataItem) { + $data[$i] = $this->name($dataItem); + } + return $data; + } + $cacheKey = $this->cacheMethodHasher($this->startQuote . $data . $this->endQuote); + if ($return = $this->cacheMethod(__FUNCTION__, $cacheKey)) { + return $return; + } + $data = trim($data); + if (preg_match('/^[\w-]+(?:\.[^ \*]*)*$/', $data)) { // string, string.string + if (strpos($data, '.') === false) { // string + return $this->cacheMethod(__FUNCTION__, $cacheKey, $this->startQuote . $data . $this->endQuote); + } + $items = explode('.', $data); + return $this->cacheMethod(__FUNCTION__, $cacheKey, + $this->startQuote . implode($this->endQuote . '.' . $this->startQuote, $items) . $this->endQuote + ); + } + if (preg_match('/^[\w-]+\.\*$/', $data)) { // string.* + return $this->cacheMethod(__FUNCTION__, $cacheKey, + $this->startQuote . str_replace('.*', $this->endQuote . '.*', $data) + ); + } + if (preg_match('/^([\w-]+)\((.*)\)$/', $data, $matches)) { // Functions + return $this->cacheMethod(__FUNCTION__, $cacheKey, + $matches[1] . '(' . $this->name($matches[2]) . ')' + ); + } + if (preg_match('/^([\w-]+(\.[\w-]+|\(.*\))*)\s+' . preg_quote($this->alias) . '\s*([\w-]+)$/i', $data, $matches)) { + return $this->cacheMethod( + __FUNCTION__, $cacheKey, + preg_replace( + '/\s{2,}/', ' ', $this->name($matches[1]) . ' ' . $this->alias . ' ' . $this->name($matches[3]) + ) + ); + } + if (preg_match('/^[\w\-_\s]*[\w\-_]+/', $data)) { + return $this->cacheMethod(__FUNCTION__, $cacheKey, $this->startQuote . $data . $this->endQuote); + } + return $this->cacheMethod(__FUNCTION__, $cacheKey, $data); + } + + /** + * Hashes a given value. + * + * Method caching uses `md5` (by default) to construct cache keys. If you have problems with collisions, + * try a different hashing algorithm or set DboSource::$cacheMethods to false. + * + * @param string $value Value to hash + * @return string Hashed value + * @see http://php.net/manual/en/function.hash-algos.php + * @see http://softwareengineering.stackexchange.com/questions/49550/which-hashing-algorithm-is-best-for-uniqueness-and-speed + */ + public function cacheMethodHasher($value) + { + return md5($value); + } + + /** + * Cache a value into the methodCaches. Will respect the value of DboSource::$cacheMethods. + * Will retrieve a value from the cache if $value is null. + * + * If caching is disabled and a write is attempted, the $value will be returned. + * A read will either return the value or null. + * + * @param string $method Name of the method being cached. + * @param string $key The key name for the cache operation. + * @param mixed $value The value to cache into memory. + * @return mixed Either null on failure, or the value if its set. + */ + public function cacheMethod($method, $key, $value = null) + { + if ($this->cacheMethods === false) { + return $value; + } + if (!$this->_methodCacheChange && empty(static::$methodCache)) { + static::$methodCache = (array)Cache::read('method_cache', '_cake_core_'); + } + if ($value === null) { + return (isset(static::$methodCache[$method][$key])) ? static::$methodCache[$method][$key] : null; + } + if (!$this->cacheMethodFilter($method, $key, $value)) { + return $value; + } + $this->_methodCacheChange = true; + return static::$methodCache[$method][$key] = $value; + } + + /** + * Filters to apply to the results of `name` and `fields`. When the filter for a given method does not return `true` + * then the result is not added to the memory cache. + * + * Some examples: + * + * ``` + * // For method fields, do not cache values that contain floats + * if ($method === 'fields') { + * $hasFloat = preg_grep('/(\d+)?\.\d+/', $value); + * + * return count($hasFloat) === 0; + * } + * + * return true; + * ``` + * + * ``` + * // For method name, do not cache values that have the name created + * if ($method === 'name') { + * return preg_match('/^`created`$/', $value) !== 1; + * } + * + * return true; + * ``` + * + * ``` + * // For method name, do not cache values that have the key 472551d38e1f8bbc78d7dfd28106166f + * if ($key === '472551d38e1f8bbc78d7dfd28106166f') { + * return false; + * } + * + * return true; + * ``` + * + * @param string $method Name of the method being cached. + * @param string $key The key name for the cache operation. + * @param mixed $value The value to cache into memory. + * @return bool Whether or not to cache + */ + public function cacheMethodFilter($method, $key, $value) + { + return true; + } + + /** + * Guesses the data type of an array + * + * @param string $value The value to introspect for type data. + * @return string + */ + public function introspectType($value) + { + if (!is_array($value)) { + if (is_bool($value)) { + return 'boolean'; + } + if (is_float($value) && (float)$value === $value) { + return 'float'; + } + if (is_int($value) && (int)$value === $value) { + return 'integer'; + } + if (is_string($value) && strlen($value) > 255) { + return 'text'; + } + return 'string'; + } + + $isAllFloat = $isAllInt = true; + $containsInt = $containsString = false; + foreach ($value as $valElement) { + $valElement = trim($valElement); + if (!is_float($valElement) && !preg_match('/^[\d]+\.[\d]+$/', $valElement)) { + $isAllFloat = false; + } else { + continue; + } + if (!is_int($valElement) && !preg_match('/^[\d]+$/', $valElement)) { + $isAllInt = false; + } else { + $containsInt = true; + continue; + } + $containsString = true; + } + + if ($isAllFloat) { + return 'float'; + } + if ($isAllInt) { + return 'integer'; + } + + if ($containsInt && !$containsString) { + return 'integer'; + } + return 'string'; + } + + /** + * Translates between PHP boolean values and Database (faked) boolean values + * + * @param mixed $data Value to be translated + * @param bool $quote Whether or not the field should be cast to a string. + * @return string|bool Converted boolean value + */ + public function boolean($data, $quote = false) + { + if ($quote) { + return !empty($data) ? '1' : '0'; + } + return !empty($data); + } + + /** + * Gets full table name including prefix + * + * @param Model|string $model Either a Model object or a string table name. + * @param bool $quote Whether you want the table name quoted. + * @param bool $schema Whether you want the schema name included. + * @return string Full quoted table name + */ + public function fullTableName($model, $quote = true, $schema = true) + { + if (is_object($model)) { + $schemaName = $model->schemaName; + $table = $model->tablePrefix . $model->table; + } else if (!empty($this->config['prefix']) && strpos($model, $this->config['prefix']) !== 0) { + $table = $this->config['prefix'] . strval($model); + } else { + $table = strval($model); + } + + if ($schema && !isset($schemaName)) { + $schemaName = $this->getSchemaName(); + } + + if ($quote) { + if ($schema && !empty($schemaName)) { + if (strstr($table, '.') === false) { + return $this->name($schemaName) . '.' . $this->name($table); + } + } + return $this->name($table); + } + + if ($schema && !empty($schemaName)) { + if (strstr($table, '.') === false) { + return $schemaName . '.' . $table; + } + } + + return $table; + } + + /** + * Renders a final SQL statement by putting together the component parts in the correct order + * + * @param string $type type of query being run. e.g select, create, update, delete, schema, alter. + * @param array $data Array of data to insert into the query. + * @return string|null Rendered SQL expression to be run, otherwise null. + */ + public function renderStatement($type, $data) + { + extract($data); + $aliases = null; + + switch (strtolower($type)) { + case 'select': + $having = !empty($having) ? " $having" : ''; + $lock = !empty($lock) ? " $lock" : ''; + return trim("SELECT {$fields} FROM {$table} {$alias} {$joins} {$conditions} {$group}{$having} {$order} {$limit}{$lock}"); + case 'create': + return "INSERT INTO {$table} ({$fields}) VALUES ({$values})"; + case 'update': + if (!empty($alias)) { + $aliases = "{$this->alias}{$alias} {$joins} "; + } + return trim("UPDATE {$table} {$aliases}SET {$fields} {$conditions}"); + case 'delete': + if (!empty($alias)) { + $aliases = "{$this->alias}{$alias} {$joins} "; + } + return trim("DELETE {$alias} FROM {$table} {$aliases}{$conditions}"); + case 'schema': + foreach (['columns', 'indexes', 'tableParameters'] as $var) { + if (is_array(${$var})) { + ${$var} = "\t" . implode(",\n\t", array_filter(${$var})); + } else { + ${$var} = ''; + } + } + if (trim($indexes) !== '') { + $columns .= ','; + } + return "CREATE TABLE {$table} (\n{$columns}{$indexes}) {$tableParameters};"; + case 'alter': + return null; + } + } + + /** + * Returns the ID generated from the previous INSERT operation. + * + * @param mixed $source The source to get an id for. + * @return mixed + */ + public function lastInsertId($source = null) + { + return $this->_connection->lastInsertId(); + } + + /** + * The "R" in CRUD + * + * Reads record(s) from the database. + * + * @param Model $Model A Model object that the query is for. + * @param array $queryData An array of queryData information containing keys similar to Model::find(). + * @param int $recursive Number of levels of association + * @return mixed boolean false on error/failure. An array of results on success. + */ + public function read(Model $Model, $queryData = [], $recursive = null) + { + $queryData = $this->_scrubQueryData($queryData); + + $array = ['callbacks' => $queryData['callbacks']]; + + if ($recursive === null && isset($queryData['recursive'])) { + $recursive = $queryData['recursive']; + } + + if ($recursive !== null) { + $modelRecursive = $Model->recursive; + $Model->recursive = $recursive; + } + + if (!empty($queryData['fields'])) { + $noAssocFields = true; + $queryData['fields'] = $this->fields($Model, null, $queryData['fields']); + } else { + $noAssocFields = false; + $queryData['fields'] = $this->fields($Model); + } + + if ($Model->recursive === -1) { + // Primary model data only, no joins. + $associations = []; + + } else { + $associations = $Model->associations(); + + if ($Model->recursive === 0) { + // Primary model data and its domain. + unset($associations[2], $associations[3]); + } + } + + $originalJoins = $queryData['joins']; + $queryData['joins'] = []; + + // Generate hasOne and belongsTo associations inside $queryData + $linkedModels = []; + foreach ($associations as $type) { + if ($type !== 'hasOne' && $type !== 'belongsTo') { + continue; + } + + foreach ($Model->{$type} as $assoc => $assocData) { + $LinkModel = $Model->{$assoc}; + + if ($Model->useDbConfig !== $LinkModel->useDbConfig) { + continue; + } + + if ($noAssocFields) { + $assocData['fields'] = false; + } + + $external = isset($assocData['external']); + + if ($this->generateAssociationQuery($Model, $LinkModel, $type, $assoc, $assocData, $queryData, $external) === true) { + $linkedModels[$type . '/' . $assoc] = true; + } + } + } + + if (!empty($originalJoins)) { + $queryData['joins'] = array_merge($queryData['joins'], $originalJoins); + } + + // Build SQL statement with the primary model, plus hasOne and belongsTo associations + $query = $this->buildAssociationQuery($Model, $queryData); + + $resultSet = $this->fetchAll($query, $Model->cacheQueries); + unset($query); + + if ($resultSet === false) { + $Model->onError(); + return false; + } + + $filtered = []; + + // Deep associations + if ($Model->recursive > -1) { + $joined = []; + if (isset($queryData['joins'][0]['alias'])) { + $joined[$Model->alias] = (array)Hash::extract($queryData['joins'], '{n}.alias'); + } + + foreach ($associations as $type) { + foreach ($Model->{$type} as $assoc => $assocData) { + $LinkModel = $Model->{$assoc}; + + if (!isset($linkedModels[$type . '/' . $assoc])) { + $db = $Model->useDbConfig === $LinkModel->useDbConfig ? $this : $LinkModel->getDataSource(); + } else if ($Model->recursive > 1) { + $db = $this; + } + + if (isset($db) && method_exists($db, 'queryAssociation')) { + $stack = [$assoc]; + $stack['_joined'] = $joined; + + $db->queryAssociation($Model, $LinkModel, $type, $assoc, $assocData, $array, true, $resultSet, $Model->recursive - 1, $stack); + unset($db); + + if ($type === 'hasMany' || $type === 'hasAndBelongsToMany') { + $filtered[] = $assoc; + } + } + } + } + } + + if ($queryData['callbacks'] === true || $queryData['callbacks'] === 'after') { + $this->_filterResults($resultSet, $Model, $filtered); + } + + if ($recursive !== null) { + $Model->recursive = $modelRecursive; + } + + return $resultSet; + } + + /** + * Private helper method to remove query metadata in given data array. + * + * @param array $data The data to scrub. + * @return array + */ + protected function _scrubQueryData($data) + { + static $base = null; + if ($base === null) { + $base = array_fill_keys(['conditions', 'fields', 'joins', 'order', 'limit', 'offset', 'group'], []); + $base['having'] = null; + $base['lock'] = null; + $base['callbacks'] = null; + } + return (array)$data + $base; + } + + /** + * Generates the fields list of an SQL query. + * + * @param Model $Model The model to get fields for. + * @param string $alias Alias table name + * @param mixed $fields The provided list of fields. + * @param bool $quote If false, returns fields array unquoted + * @return array + */ + public function fields(Model $Model, $alias = null, $fields = [], $quote = true) + { + if (empty($alias)) { + $alias = $Model->alias; + } + $virtualFields = $Model->getVirtualField(); + $cacheKey = [ + $alias, + get_class($Model), + $Model->alias, + $virtualFields, + $fields, + $quote, + ConnectionManager::getSourceName($this), + $Model->schemaName, + $Model->table + ]; + $cacheKey = $this->cacheMethodHasher(serialize($cacheKey)); + if ($return = $this->cacheMethod(__FUNCTION__, $cacheKey)) { + return $return; + } + $allFields = empty($fields); + if ($allFields) { + $fields = array_keys($Model->schema()); + } else if (!is_array($fields)) { + $fields = CakeText::tokenize($fields); + } + $fields = array_values(array_filter($fields)); + $allFields = $allFields || in_array('*', $fields) || in_array($Model->alias . '.*', $fields); + + $virtual = []; + if (!empty($virtualFields)) { + $virtualKeys = array_keys($virtualFields); + foreach ($virtualKeys as $field) { + $virtualKeys[] = $Model->alias . '.' . $field; + } + $virtual = ($allFields) ? $virtualKeys : array_intersect($virtualKeys, $fields); + foreach ($virtual as $i => $field) { + if (strpos($field, '.') !== false) { + $virtual[$i] = str_replace($Model->alias . '.', '', $field); + } + $fields = array_diff($fields, [$field]); + } + $fields = array_values($fields); + } + if (!$quote) { + if (!empty($virtual)) { + $fields = array_merge($fields, $this->_constructVirtualFields($Model, $alias, $virtual)); + } + return $fields; + } + $count = count($fields); + + if ($count >= 1 && !in_array($fields[0], ['*', 'COUNT(*)'])) { + for ($i = 0; $i < $count; $i++) { + if (is_string($fields[$i]) && in_array($fields[$i], $virtual)) { + unset($fields[$i]); + continue; + } + if (is_object($fields[$i]) && isset($fields[$i]->type) && $fields[$i]->type === 'expression') { + $fields[$i] = $fields[$i]->value; + } else if (preg_match('/^\(.*\)\s' . $this->alias . '.*/i', $fields[$i])) { + continue; + } else if (!preg_match('/^.+\\(.*\\)/', $fields[$i])) { + $prepend = ''; + + if (strpos($fields[$i], 'DISTINCT') !== false) { + $prepend = 'DISTINCT '; + $fields[$i] = trim(str_replace('DISTINCT', '', $fields[$i])); + } + $dot = strpos($fields[$i], '.'); + + if ($dot === false) { + $prefix = !( + strpos($fields[$i], ' ') !== false || + strpos($fields[$i], '(') !== false + ); + $fields[$i] = $this->name(($prefix ? $alias . '.' : '') . $fields[$i]); + } else { + if (strpos($fields[$i], ',') === false) { + $build = explode('.', $fields[$i]); + if (!Hash::numeric($build)) { + $fields[$i] = $this->name(implode('.', $build)); + } + } + } + $fields[$i] = $prepend . $fields[$i]; + } else if (preg_match('/\(([\.\w]+)\)/', $fields[$i], $field)) { + if (isset($field[1])) { + if (strpos($field[1], '.') === false) { + $field[1] = $this->name($alias . '.' . $field[1]); + } else { + $field[0] = explode('.', $field[1]); + if (!Hash::numeric($field[0])) { + $field[0] = implode('.', array_map([&$this, 'name'], $field[0])); + $fields[$i] = preg_replace('/\(' . $field[1] . '\)/', '(' . $field[0] . ')', $fields[$i], 1); + } + } + } + } + } + } + if (!empty($virtual)) { + $fields = array_merge($fields, $this->_constructVirtualFields($Model, $alias, $virtual)); + } + return $this->cacheMethod(__FUNCTION__, $cacheKey, array_unique($fields)); + } + + /** + * Converts model virtual fields into sql expressions to be fetched later + * + * @param Model $Model The model to get virtual fields for. + * @param string $alias Alias table name + * @param array $fields virtual fields to be used on query + * @return array + */ + protected function _constructVirtualFields(Model $Model, $alias, $fields) + { + $virtual = []; + foreach ($fields as $field) { + $virtualField = $this->name($alias . $this->virtualFieldSeparator . $field); + $virtualFieldExpression = $Model->getVirtualField($field); + if (is_object($virtualFieldExpression) && $virtualFieldExpression->type == 'expression') { + $expression = $virtualFieldExpression->value; + } else { + $expression = $this->_quoteFields($virtualFieldExpression); + } + $virtual[] = '(' . $expression . ") {$this->alias} {$virtualField}"; + } + return $virtual; + } + + /** + * Quotes Model.fields + * + * @param string $conditions The conditions to quote. + * @return string or false if no match + */ + protected function _quoteFields($conditions) + { + $start = $end = null; + $original = $conditions; + + if (!empty($this->startQuote)) { + $start = preg_quote($this->startQuote); + } + if (!empty($this->endQuote)) { + $end = preg_quote($this->endQuote); + } + + // Remove quotes and requote all the Model.field names. + $conditions = str_replace([$start, $end], '', $conditions); + $conditions = preg_replace_callback( + '/(?:[\'\"][^\'\"\\\]*(?:\\\.[^\'\"\\\]*)*[\'\"])|([a-z0-9_][a-z0-9\\-_]*\\.[a-z0-9_][a-z0-9_\\-]*[a-z0-9_])|([a-z0-9_][a-z0-9_\\-]*)(?=->)/i', + [&$this, '_quoteMatchedField'], + $conditions + ); + // Quote `table_name AS Alias` + $conditions = preg_replace( + '/(\s[a-z0-9\\-_.' . $start . $end . ']*' . $end . ')\s+AS\s+([a-z0-9\\-_]+)/i', + '\1 AS ' . $this->startQuote . '\2' . $this->endQuote, + $conditions + ); + if ($conditions !== null) { + return $conditions; + } + return $original; + } + + /** + * Generates a query or part of a query from a single model or two associated models. + * + * Builds a string containing an SQL statement template. + * + * @param Model $Model Primary Model object. + * @param Model|null $LinkModel Linked model object. + * @param string $type Association type, one of the model association types ie. hasMany. + * @param string $association Association name. + * @param array $assocData Association data. + * @param array &$queryData An array of queryData information containing keys similar to Model::find(). + * @param bool $external Whether or not the association query is on an external datasource. + * @return mixed + * String representing a query. + * True, when $external is false and association $type is 'hasOne' or 'belongsTo'. + */ + public function generateAssociationQuery(Model $Model, $LinkModel, $type, $association, $assocData, &$queryData, $external) + { + $assocData = $this->_scrubQueryData($assocData); + $queryData = $this->_scrubQueryData($queryData); + + if ($LinkModel === null) { + return $this->buildStatement( + [ + 'fields' => array_unique($queryData['fields']), + 'table' => $this->fullTableName($Model), + 'alias' => $Model->alias, + 'limit' => $queryData['limit'], + 'offset' => $queryData['offset'], + 'joins' => $queryData['joins'], + 'conditions' => $queryData['conditions'], + 'order' => $queryData['order'], + 'group' => $queryData['group'] + ], + $Model + ); + } + + if ($external && !empty($assocData['finderQuery'])) { + return $assocData['finderQuery']; + } + + if ($type === 'hasMany' || $type === 'hasAndBelongsToMany') { + if (empty($assocData['offset']) && !empty($assocData['page'])) { + $assocData['offset'] = ($assocData['page'] - 1) * $assocData['limit']; + } + } + + switch ($type) { + case 'hasOne': + case 'belongsTo': + $conditions = $this->_mergeConditions( + $assocData['conditions'], + $this->getConstraint($type, $Model, $LinkModel, $association, array_merge($assocData, compact('external'))) + ); + + if ($external) { + // Not self join + if ($Model->name !== $LinkModel->name) { + $modelAlias = $Model->alias; + foreach ($conditions as $key => $condition) { + if (is_numeric($key) && strpos($condition, $modelAlias . '.') !== false) { + unset($conditions[$key]); + } + } + } + + $query = array_merge($assocData, [ + 'conditions' => $conditions, + 'table' => $this->fullTableName($LinkModel), + 'fields' => $this->fields($LinkModel, $association, $assocData['fields']), + 'alias' => $association, + 'group' => null + ]); + } else { + $join = [ + 'table' => $LinkModel, + 'alias' => $association, + 'type' => isset($assocData['type']) ? $assocData['type'] : 'LEFT', + 'conditions' => trim($this->conditions($conditions, true, false, $Model)) + ]; + + $fields = []; + if ($assocData['fields'] !== false) { + $fields = $this->fields($LinkModel, $association, $assocData['fields']); + } + + $queryData['fields'] = array_merge($this->prepareFields($Model, $queryData), $fields); + + if (!empty($assocData['order'])) { + $queryData['order'][] = $assocData['order']; + } + if (!in_array($join, $queryData['joins'], true)) { + $queryData['joins'][] = $join; + } + + return true; + } + break; + case 'hasMany': + $assocData['fields'] = $this->fields($LinkModel, $association, $assocData['fields']); + if (!empty($assocData['foreignKey'])) { + $assocData['fields'] = array_merge($assocData['fields'], $this->fields($LinkModel, $association, ["{$association}.{$assocData['foreignKey']}"])); + } + + $query = [ + 'conditions' => $this->_mergeConditions($this->getConstraint('hasMany', $Model, $LinkModel, $association, $assocData), $assocData['conditions']), + 'fields' => array_unique($assocData['fields']), + 'table' => $this->fullTableName($LinkModel), + 'alias' => $association, + 'order' => $assocData['order'], + 'limit' => $assocData['limit'], + 'offset' => $assocData['offset'], + 'group' => null + ]; + break; + case 'hasAndBelongsToMany': + $joinFields = []; + $joinAssoc = null; + + if (isset($assocData['with']) && !empty($assocData['with'])) { + $joinKeys = [$assocData['foreignKey'], $assocData['associationForeignKey']]; + list($with, $joinFields) = $Model->joinModel($assocData['with'], $joinKeys); + + $joinTbl = $Model->{$with}; + $joinAlias = $joinTbl; + + if (is_array($joinFields) && !empty($joinFields)) { + $joinAssoc = $joinAlias = $joinTbl->alias; + $joinFields = $this->fields($joinTbl, $joinAlias, $joinFields); + } else { + $joinFields = []; + } + } else { + $joinTbl = $assocData['joinTable']; + $joinAlias = $this->fullTableName($assocData['joinTable']); + } + + $query = [ + 'conditions' => $assocData['conditions'], + 'limit' => $assocData['limit'], + 'offset' => $assocData['offset'], + 'table' => $this->fullTableName($LinkModel), + 'alias' => $association, + 'fields' => array_merge($this->fields($LinkModel, $association, $assocData['fields']), $joinFields), + 'order' => $assocData['order'], + 'group' => null, + 'joins' => [[ + 'table' => $joinTbl, + 'alias' => $joinAssoc, + 'conditions' => $this->getConstraint('hasAndBelongsToMany', $Model, $LinkModel, $joinAlias, $assocData, $association) + ]] + ]; + break; + } + + if (isset($query)) { + return $this->buildStatement($query, $Model); + } + + return null; + } + + /** + * Builds and generates an SQL statement from an array. Handles final clean-up before conversion. + * + * @param array $query An array defining an SQL query. + * @param Model $Model The model object which initiated the query. + * @return string An executable SQL statement. + * @see DboSource::renderStatement() + */ + public function buildStatement($query, Model $Model) + { + $query = array_merge($this->_queryDefaults, $query); + + if (!empty($query['joins'])) { + $count = count($query['joins']); + for ($i = 0; $i < $count; $i++) { + if (is_array($query['joins'][$i])) { + $query['joins'][$i] = $this->buildJoinStatement($query['joins'][$i]); + } + } + } + + return $this->renderStatement('select', [ + 'conditions' => $this->conditions($query['conditions'], true, true, $Model), + 'fields' => implode(', ', $query['fields']), + 'table' => $query['table'], + 'alias' => $this->alias . $this->name($query['alias']), + 'order' => $this->order($query['order'], 'ASC', $Model), + 'limit' => $this->limit($query['limit'], $query['offset']), + 'joins' => implode(' ', $query['joins']), + 'group' => $this->group($query['group'], $Model), + 'having' => $this->having($query['having'], true, $Model), + 'lock' => $this->getLockingHint($query['lock']), + ]); + } + + /** + * Builds and generates a JOIN condition from an array. Handles final clean-up before conversion. + * + * @param array $join An array defining a JOIN condition in a query. + * @return string An SQL JOIN condition to be used in a query. + * @see DboSource::renderJoinStatement() + * @see DboSource::buildStatement() + */ + public function buildJoinStatement($join) + { + $data = array_merge([ + 'type' => null, + 'alias' => null, + 'table' => 'join_table', + 'conditions' => '', + ], $join); + + if (!empty($data['alias'])) { + $data['alias'] = $this->alias . $this->name($data['alias']); + } + if (!empty($data['conditions'])) { + $data['conditions'] = trim($this->conditions($data['conditions'], true, false)); + } + if (!empty($data['table']) && (!is_string($data['table']) || strpos($data['table'], '(') !== 0)) { + $data['table'] = $this->fullTableName($data['table']); + } + return $this->renderJoinStatement($data); + } + + /** + * Creates a WHERE clause by parsing given conditions data. If an array or string + * conditions are provided those conditions will be parsed and quoted. If a boolean + * is given it will be integer cast as condition. Null will return 1 = 1. + * + * Results of this method are stored in a memory cache. This improves performance, but + * because the method uses a hashing algorithm it can have collisions. + * Setting DboSource::$cacheMethods to false will disable the memory cache. + * + * @param mixed $conditions Array or string of conditions, or any value. + * @param bool $quoteValues If true, values should be quoted + * @param bool $where If true, "WHERE " will be prepended to the return value + * @param Model $Model A reference to the Model instance making the query + * @return string SQL fragment + */ + public function conditions($conditions, $quoteValues = true, $where = true, Model $Model = null) + { + $clause = $out = ''; + + if ($where) { + $clause = ' WHERE '; + } + + if (is_array($conditions) && !empty($conditions)) { + $out = $this->conditionKeysToString($conditions, $quoteValues, $Model); + + if (empty($out)) { + return $clause . ' 1 = 1'; + } + return $clause . implode(' AND ', $out); + } + + if (is_bool($conditions)) { + return $clause . (int)$conditions . ' = 1'; + } + + if (empty($conditions) || trim($conditions) === '') { + return $clause . '1 = 1'; + } + + $clauses = '/^WHERE\\x20|^GROUP\\x20BY\\x20|^HAVING\\x20|^ORDER\\x20BY\\x20/i'; + + if (preg_match($clauses, $conditions)) { + $clause = ''; + } + + $conditions = $this->_quoteFields($conditions); + + return $clause . $conditions; + } + + /** + * Creates a WHERE clause by parsing given conditions array. Used by DboSource::conditions(). + * + * @param array $conditions Array or string of conditions + * @param bool $quoteValues If true, values should be quoted + * @param Model $Model A reference to the Model instance making the query + * @return string SQL fragment + */ + public function conditionKeysToString($conditions, $quoteValues = true, Model $Model = null) + { + $out = []; + $data = $columnType = null; + + foreach ($conditions as $key => $value) { + $join = ' AND '; + $not = null; + + if (is_array($value)) { + $valueInsert = ( + !empty($value) && + (substr_count($key, '?') === count($value) || substr_count($key, ':') === count($value)) + ); + } + + if (is_numeric($key) && empty($value)) { + continue; + } else if (is_numeric($key) && is_string($value)) { + $out[] = $this->_quoteFields($value); + } else if ((is_numeric($key) && is_array($value)) || in_array(strtolower(trim($key)), $this->_sqlBoolOps)) { + if (in_array(strtolower(trim($key)), $this->_sqlBoolOps)) { + $join = ' ' . strtoupper($key) . ' '; + } else { + $key = $join; + } + $value = $this->conditionKeysToString($value, $quoteValues, $Model); + + if (strpos($join, 'NOT') !== false) { + if (strtoupper(trim($key)) === 'NOT') { + $key = 'AND ' . trim($key); + } + $not = 'NOT '; + } + + if (empty($value)) { + continue; + } + + if (empty($value[1])) { + if ($not) { + $out[] = $not . '(' . $value[0] . ')'; + } else { + $out[] = $value[0]; + } + } else { + $out[] = '(' . $not . '(' . implode(') ' . strtoupper($key) . ' (', $value) . '))'; + } + } else { + if (is_object($value) && isset($value->type)) { + if ($value->type === 'identifier') { + $data .= $this->name($key) . ' = ' . $this->name($value->value); + } else if ($value->type === 'expression') { + if (is_numeric($key)) { + $data .= $value->value; + } else { + $data .= $this->name($key) . ' = ' . $value->value; + } + } + } else if (is_array($value) && !empty($value) && !$valueInsert) { + $keys = array_keys($value); + if ($keys === array_values($keys)) { + if (count($value) === 1 && !preg_match('/\s+(?:NOT|IN|\!=)$/', $key)) { + $data = $this->_quoteFields($key) . ' = ('; + if ($quoteValues) { + if ($Model !== null) { + $columnType = $Model->getColumnType($key); + } + $data .= implode(', ', $this->value($value, $columnType)); + } + $data .= ')'; + } else { + $data = $this->_parseKey($key, $value, $Model); + } + } else { + $ret = $this->conditionKeysToString($value, $quoteValues, $Model); + if (count($ret) > 1) { + $data = '(' . implode(') AND (', $ret) . ')'; + } else if (isset($ret[0])) { + $data = $ret[0]; + } + } + } else if (is_numeric($key) && !empty($value)) { + $data = $this->_quoteFields($value); + } else { + $data = $this->_parseKey(trim($key), $value, $Model); + } + + if ($data) { + $out[] = $data; + $data = null; + } + } + } + return $out; + } + + /** + * Extracts a Model.field identifier and an SQL condition operator from a string, formats + * and inserts values, and composes them into an SQL snippet. + * + * @param string $key An SQL key snippet containing a field and optional SQL operator + * @param mixed $value The value(s) to be inserted in the string + * @param Model $Model Model object initiating the query + * @return string + */ + protected function _parseKey($key, $value, Model $Model = null) + { + $operatorMatch = '/^(((' . implode(')|(', $this->_sqlOps); + $operatorMatch .= ')\\x20?)|<[>=]?(?![^>]+>)\\x20?|[>=!]{1,3}(?!<)\\x20?)/is'; + $bound = (strpos($key, '?') !== false || (is_array($value) && strpos($key, ':') !== false)); + + $key = trim($key); + if (strpos($key, ' ') === false) { + $operator = '='; + } else { + list($key, $operator) = explode(' ', $key, 2); + + if (!preg_match($operatorMatch, trim($operator)) && strpos($operator, ' ') !== false) { + $key = $key . ' ' . $operator; + $split = strrpos($key, ' '); + $operator = substr($key, $split); + $key = substr($key, 0, $split); + } + } + + $virtual = false; + $type = null; + + if ($Model !== null) { + if ($Model->isVirtualField($key)) { + $virtualField = $Model->getVirtualField($key); + if (is_object($virtualField) && $virtualField->type == 'expression') { + $key = $virtualField->value; + } else { + $key = $this->_quoteFields($virtualField); + } + $virtual = true; + } + + $type = $Model->getColumnType($key); + } + + $null = $value === null || (is_array($value) && empty($value)); + + if (strtolower($operator) === 'not') { + $data = $this->conditionKeysToString( + [$operator => [$key => $value]], true, $Model + ); + return $data[0]; + } + + $value = $this->value($value, $type); + + if (!$virtual && $key !== '?') { + $isKey = ( + strpos($key, '(') !== false || + strpos($key, ')') !== false || + strpos($key, '|') !== false || + strpos($key, '->') !== false + ); + $key = $isKey ? $this->_quoteFields($key) : $this->name($key); + } + + if ($bound) { + return CakeText::insert($key . ' ' . trim($operator), $value); + } + + if (!preg_match($operatorMatch, trim($operator))) { + $operator .= is_array($value) ? ' IN' : ' ='; + } + $operator = trim($operator); + + if (is_array($value)) { + $value = implode(', ', $value); + + switch ($operator) { + case '=': + $operator = 'IN'; + break; + case '!=': + case '<>': + $operator = 'NOT IN'; + break; + } + $value = "({$value})"; + } else if ($null || $value === 'NULL') { + switch ($operator) { + case '=': + $operator = 'IS'; + break; + case '!=': + case '<>': + $operator = 'IS NOT'; + break; + } + } + if ($virtual) { + return "({$key}) {$operator} {$value}"; + } + return "{$key} {$operator} {$value}"; + } + + /** + * Renders a final SQL JOIN statement + * + * @param array $data The data to generate a join statement for. + * @return string + */ + public function renderJoinStatement($data) + { + if (strtoupper($data['type']) === 'CROSS' || empty($data['conditions'])) { + return "{$data['type']} JOIN {$data['table']} {$data['alias']}"; + } + return trim("{$data['type']} JOIN {$data['table']} {$data['alias']} ON ({$data['conditions']})"); + } + + /** + * Returns an ORDER BY clause as a string. + * + * @param array|string $keys Field reference, as a key (i.e. Post.title) + * @param string $direction Direction (ASC or DESC) + * @param Model $Model Model reference (used to look for virtual field) + * @return string ORDER BY clause + */ + public function order($keys, $direction = 'ASC', Model $Model = null) + { + if (!is_array($keys)) { + $keys = [$keys]; + } + $keys = array_filter($keys); + + $result = []; + while (!empty($keys)) { + $key = key($keys); + $dir = current($keys); + array_shift($keys); + + if (is_numeric($key)) { + $key = $dir; + $dir = $direction; + } + + if (is_string($key) && strpos($key, ',') !== false && !preg_match('/\(.+\,.+\)/', $key)) { + $key = array_map('trim', explode(',', $key)); + } + + if (is_array($key)) { + //Flatten the array + $key = array_reverse($key, true); + foreach ($key as $k => $v) { + if (is_numeric($k)) { + array_unshift($keys, $v); + } else { + $keys = [$k => $v] + $keys; + } + } + continue; + } else if (is_object($key) && isset($key->type) && $key->type === 'expression') { + $result[] = $key->value; + continue; + } + + if (preg_match('/\\x20(ASC|DESC).*/i', $key, $_dir)) { + $dir = $_dir[0]; + $key = preg_replace('/\\x20(ASC|DESC).*/i', '', $key); + } + + $key = trim($key); + + if ($Model !== null) { + if ($Model->isVirtualField($key)) { + $key = '(' . $this->_quoteFields($Model->getVirtualField($key)) . ')'; + } + + list($alias) = pluginSplit($key); + + if ($alias !== $Model->alias && is_object($Model->{$alias}) && $Model->{$alias}->isVirtualField($key)) { + $key = '(' . $this->_quoteFields($Model->{$alias}->getVirtualField($key)) . ')'; + } + } + + if (strpos($key, '.')) { + $key = preg_replace_callback('/([a-zA-Z0-9_-]{1,})\\.([a-zA-Z0-9_-]{1,})/', [&$this, '_quoteMatchedField'], $key); + } + + if (!preg_match('/\s/', $key) && strpos($key, '.') === false) { + $key = $this->name($key); + } + + $key .= ' ' . trim($dir); + + $result[] = $key; + } + + if (!empty($result)) { + return ' ORDER BY ' . implode(', ', $result); + } + + return ''; + } + + /** + * Returns a limit statement in the correct format for the particular database. + * + * @param int $limit Limit of results returned + * @param int $offset Offset from which to start results + * @return string SQL limit/offset statement + */ + public function limit($limit, $offset = null) + { + if ($limit) { + $rt = ' LIMIT'; + + if ($offset) { + $rt .= sprintf(' %u,', $offset); + } + + $rt .= sprintf(' %u', $limit); + return $rt; + } + return null; + } + + /** + * Create a GROUP BY SQL clause. + * + * @param string|array $fields Group By fields + * @param Model $Model The model to get group by fields for. + * @return string Group By clause or null. + */ + public function group($fields, Model $Model = null) + { + if (empty($fields)) { + return null; + } + + if (!is_array($fields)) { + $fields = [$fields]; + } + + if ($Model !== null) { + foreach ($fields as $index => $key) { + if ($Model->isVirtualField($key)) { + $fields[$index] = '(' . $Model->getVirtualField($key) . ')'; + } + } + } + + $fields = implode(', ', $fields); + + return ' GROUP BY ' . $this->_quoteFields($fields); + } + + /** + * Create a HAVING SQL clause. + * + * @param mixed $fields Array or string of conditions + * @param bool $quoteValues If true, values should be quoted + * @param Model $Model A reference to the Model instance making the query + * @return string|null HAVING clause or null + */ + public function having($fields, $quoteValues = true, Model $Model = null) + { + if (!$fields) { + return null; + } + return ' HAVING ' . $this->conditions($fields, $quoteValues, false, $Model); + } + + /** + * Returns a locking hint for the given mode. + * + * Currently, this method only returns FOR UPDATE when the mode is set to true. + * + * @param mixed $mode Lock mode + * @return string|null FOR UPDATE clause or null + */ + public function getLockingHint($mode) + { + if ($mode !== true) { + return null; + } + return ' FOR UPDATE'; + } + + /** + * Merges a mixed set of string/array conditions. + * + * @param mixed $query The query to merge conditions for. + * @param mixed $assoc The association names. + * @return array + */ + protected function _mergeConditions($query, $assoc) + { + if (empty($assoc)) { + return $query; + } + + if (is_array($query)) { + return array_merge((array)$assoc, $query); + } + + if (!empty($query)) { + $query = [$query]; + if (is_array($assoc)) { + $query = array_merge($query, $assoc); + } else { + $query[] = $assoc; + } + return $query; + } + + return $assoc; + } + + /** + * Returns a conditions array for the constraint between two models. + * + * @param string $type Association type. + * @param Model $Model Primary Model object. + * @param Model $LinkModel Linked model object. + * @param string $association Association name. + * @param array $assocData Association data. + * @param string $association2 HABTM association name. + * @return array Conditions array defining the constraint between $Model and $LinkModel. + */ + public function getConstraint($type, Model $Model, Model $LinkModel, $association, $assocData, $association2 = null) + { + $assocData += ['external' => false]; + + if (empty($assocData['foreignKey'])) { + return []; + } + + switch ($type) { + case 'hasOne': + if ($assocData['external']) { + return [ + "{$association}.{$assocData['foreignKey']}" => '{$__cakeID__$}' + ]; + } else { + return [ + "{$association}.{$assocData['foreignKey']}" => $this->identifier("{$Model->alias}.{$Model->primaryKey}") + ]; + } + case 'belongsTo': + if ($assocData['external']) { + return [ + "{$association}.{$LinkModel->primaryKey}" => '{$__cakeForeignKey__$}' + ]; + } else { + return [ + "{$Model->alias}.{$assocData['foreignKey']}" => $this->identifier("{$association}.{$LinkModel->primaryKey}") + ]; + } + case 'hasMany': + return ["{$association}.{$assocData['foreignKey']}" => ['{$__cakeID__$}']]; + case 'hasAndBelongsToMany': + return [ + [ + "{$association}.{$assocData['foreignKey']}" => '{$__cakeID__$}' + ], + [ + "{$association}.{$assocData['associationForeignKey']}" => $this->identifier("{$association2}.{$LinkModel->primaryKey}") + ] + ]; + } + + return []; + } + + /** + * Returns an object to represent a database identifier in a query. Expression objects + * are not sanitized or escaped. + * + * @param string $identifier A SQL expression to be used as an identifier + * @return stdClass An object representing a database identifier to be used in a query + */ + public function identifier($identifier) + { + $obj = new stdClass(); + $obj->type = 'identifier'; + $obj->value = $identifier; + return $obj; + } + + /** + * Prepares fields required by an SQL statement. + * + * When no fields are set, all the $Model fields are returned. + * + * @param Model $Model The model to prepare. + * @param array $queryData An array of queryData information containing keys similar to Model::find(). + * @return array Array containing SQL fields. + */ + public function prepareFields(Model $Model, $queryData) + { + if (empty($queryData['fields'])) { + $queryData['fields'] = $this->fields($Model); + + } else if (!empty($Model->hasMany) && $Model->recursive > -1) { + // hasMany relationships need the $Model primary key. + $assocFields = $this->fields($Model, null, "{$Model->alias}.{$Model->primaryKey}"); + $passedFields = $queryData['fields']; + + if (count($passedFields) > 1 || + (strpos($passedFields[0], $assocFields[0]) === false && !preg_match('/^[a-z]+\(/i', $passedFields[0])) + ) { + $queryData['fields'] = array_merge($passedFields, $assocFields); + } + } + + return array_unique($queryData['fields']); + } + + /** + * Builds an SQL statement. + * + * This is merely a convenient wrapper to DboSource::buildStatement(). + * + * @param Model $Model The model to build an association query for. + * @param array $queryData An array of queryData information containing keys similar to Model::find(). + * @return string String containing an SQL statement. + * @see DboSource::buildStatement() + */ + public function buildAssociationQuery(Model $Model, $queryData) + { + $queryData = $this->_scrubQueryData($queryData); + + return $this->buildStatement( + [ + 'fields' => $this->prepareFields($Model, $queryData), + 'table' => $this->fullTableName($Model), + 'alias' => $Model->alias, + 'limit' => $queryData['limit'], + 'offset' => $queryData['offset'], + 'joins' => $queryData['joins'], + 'conditions' => $queryData['conditions'], + 'order' => $queryData['order'], + 'group' => $queryData['group'], + 'having' => $queryData['having'], + 'lock' => $queryData['lock'], + ], + $Model + ); + } + + /** + * Queries associations. + * + * Used to fetch results on recursive models. + * + * - 'hasMany' associations with no limit set: + * Fetch, filter and merge is done recursively for every level. + * + * - 'hasAndBelongsToMany' associations: + * Fetch and filter is done unaffected by the (recursive) level set. + * + * @param Model $Model Primary Model object. + * @param Model $LinkModel Linked model object. + * @param string $type Association type, one of the model association types ie. hasMany. + * @param string $association Association name. + * @param array $assocData Association data. + * @param array &$queryData An array of queryData information containing keys similar to Model::find(). + * @param bool $external Whether or not the association query is on an external datasource. + * @param array &$resultSet Existing results. + * @param int $recursive Number of levels of association. + * @param array $stack A list with joined models. + * @return mixed + * @throws CakeException when results cannot be created. + */ + public function queryAssociation(Model $Model, Model $LinkModel, $type, $association, $assocData, &$queryData, $external, &$resultSet, $recursive, $stack) + { + if (isset($stack['_joined'])) { + $joined = $stack['_joined']; + unset($stack['_joined']); + } + + $queryTemplate = $this->generateAssociationQuery($Model, $LinkModel, $type, $association, $assocData, $queryData, $external); + if (empty($queryTemplate)) { + return null; + } + + if (!is_array($resultSet)) { + throw new CakeException(__d('cake_dev', 'Error in Model %s', get_class($Model))); + } + + if ($type === 'hasMany' && empty($assocData['limit']) && !empty($assocData['foreignKey'])) { + // 'hasMany' associations with no limit set. + + $assocIds = []; + foreach ($resultSet as $result) { + $assocIds[] = $this->insertQueryData('{$__cakeID__$}', $result, $association, $Model, $stack); + } + $assocIds = array_filter($assocIds); + + // Fetch + $assocResultSet = []; + if (!empty($assocIds)) { + $assocResultSet = $this->_fetchHasMany($Model, $queryTemplate, $assocIds); + } + + // Recursively query associations + if ($recursive > 0 && !empty($assocResultSet) && is_array($assocResultSet)) { + foreach ($LinkModel->associations() as $type1) { + foreach ($LinkModel->{$type1} as $assoc1 => $assocData1) { + $DeepModel = $LinkModel->{$assoc1}; + $tmpStack = $stack; + $tmpStack[] = $assoc1; + + $db = $LinkModel->useDbConfig === $DeepModel->useDbConfig ? $this : $DeepModel->getDataSource(); + + $db->queryAssociation($LinkModel, $DeepModel, $type1, $assoc1, $assocData1, $queryData, true, $assocResultSet, $recursive - 1, $tmpStack); + } + } + } + + // Filter + if ($queryData['callbacks'] === true || $queryData['callbacks'] === 'after') { + $this->_filterResultsInclusive($assocResultSet, $Model, [$association]); + } + + // Merge + return $this->_mergeHasMany($resultSet, $assocResultSet, $association, $Model); + + } else if ($type === 'hasAndBelongsToMany') { + // 'hasAndBelongsToMany' associations. + + $assocIds = []; + foreach ($resultSet as $result) { + $assocIds[] = $this->insertQueryData('{$__cakeID__$}', $result, $association, $Model, $stack); + } + $assocIds = array_filter($assocIds); + + // Fetch + $assocResultSet = []; + if (!empty($assocIds)) { + $assocResultSet = $this->_fetchHasAndBelongsToMany($Model, $queryTemplate, $assocIds, $association); + } + + $habtmAssocData = $Model->hasAndBelongsToMany[$association]; + $foreignKey = $habtmAssocData['foreignKey']; + $joinKeys = [$foreignKey, $habtmAssocData['associationForeignKey']]; + list($with, $habtmFields) = $Model->joinModel($habtmAssocData['with'], $joinKeys); + $habtmFieldsCount = count($habtmFields); + + // Filter + if ($queryData['callbacks'] === true || $queryData['callbacks'] === 'after') { + $this->_filterResultsInclusive($assocResultSet, $Model, [$association, $with]); + } + } + + $modelAlias = $Model->alias; + $primaryKey = $Model->primaryKey; + $selfJoin = ($Model->name === $LinkModel->name); + + foreach ($resultSet as &$row) { + if ($type === 'hasOne' || $type === 'belongsTo' || $type === 'hasMany') { + $assocResultSet = []; + $prefetched = false; + + if (($type === 'hasOne' || $type === 'belongsTo') && + isset($row[$LinkModel->alias], $joined[$Model->alias]) && + in_array($LinkModel->alias, $joined[$Model->alias]) + ) { + $joinedData = Hash::filter($row[$LinkModel->alias]); + if (!empty($joinedData)) { + $assocResultSet[0] = [$LinkModel->alias => $row[$LinkModel->alias]]; + } + $prefetched = true; + } else { + $query = $this->insertQueryData($queryTemplate, $row, $association, $Model, $stack); + if ($query !== false) { + $assocResultSet = $this->fetchAll($query, $Model->cacheQueries); + } + } + } + + if (!empty($assocResultSet) && is_array($assocResultSet)) { + if ($recursive > 0) { + foreach ($LinkModel->associations() as $type1) { + foreach ($LinkModel->{$type1} as $assoc1 => $assocData1) { + $DeepModel = $LinkModel->{$assoc1}; + + if ($type1 === 'belongsTo' || + ($type === 'belongsTo' && $DeepModel->alias === $modelAlias) || + ($DeepModel->alias !== $modelAlias) + ) { + $tmpStack = $stack; + $tmpStack[] = $assoc1; + + $db = $LinkModel->useDbConfig === $DeepModel->useDbConfig ? $this : $DeepModel->getDataSource(); + + $db->queryAssociation($LinkModel, $DeepModel, $type1, $assoc1, $assocData1, $queryData, true, $assocResultSet, $recursive - 1, $tmpStack); + } + } + } + } + + if ($type === 'hasAndBelongsToMany') { + $merge = []; + foreach ($assocResultSet as $data) { + if (isset($data[$with]) && $data[$with][$foreignKey] === $row[$modelAlias][$primaryKey]) { + if ($habtmFieldsCount <= 2) { + unset($data[$with]); + } + $merge[] = $data; + } + } + + if (empty($merge) && !isset($row[$association])) { + $row[$association] = $merge; + } else { + $this->_mergeAssociation($row, $merge, $association, $type); + } + } else { + if (!$prefetched && $LinkModel->useConsistentAfterFind) { + if ($queryData['callbacks'] === true || $queryData['callbacks'] === 'after') { + $this->_filterResultsInclusive($assocResultSet, $Model, [$association]); + } + } + $this->_mergeAssociation($row, $assocResultSet, $association, $type, $selfJoin); + } + + if ($type !== 'hasAndBelongsToMany' && isset($row[$association]) && !$prefetched && !$LinkModel->useConsistentAfterFind) { + $row[$association] = $LinkModel->afterFind($row[$association], false); + } + + } else { + $tempArray[0][$association] = false; + $this->_mergeAssociation($row, $tempArray, $association, $type, $selfJoin); + } + } + } + + /** + * Fetch 'hasMany' associations. + * + * @param Model $Model Primary model object. + * @param string $query Association query template. + * @param array $ids Array of IDs of associated records. + * @return array Association results. + */ + protected function _fetchHasMany(Model $Model, $query, $ids) + { + $ids = array_unique($ids); + + if (count($ids) > 1) { + $query = str_replace('= ({$__cakeID__$}', 'IN ({$__cakeID__$}', $query); + } + $query = str_replace('{$__cakeID__$}', implode(', ', $ids), $query); + return $this->fetchAll($query, $Model->cacheQueries); + } + + /** + * Passes association results through afterFind filters of the corresponding model. + * + * Similar to DboSource::_filterResults(), but this filters only specified models. + * The primary model can not be specified, because this call DboSource::_filterResults() internally. + * + * @param array &$resultSet Reference of resultset to be filtered. + * @param Model $Model Instance of model to operate against. + * @param array $toBeFiltered List of classes to be filtered. + * @return array Array of results that have been filtered through $Model->afterFind. + */ + protected function _filterResultsInclusive(&$resultSet, Model $Model, $toBeFiltered = []) + { + $exclude = []; + + if (is_array($resultSet)) { + $current = reset($resultSet); + if (is_array($current)) { + $exclude = array_diff(array_keys($current), $toBeFiltered); + } + } + + return $this->_filterResults($resultSet, $Model, $exclude); + } + + /** + * Passes association results through afterFind filters of the corresponding model. + * + * The primary model is always excluded, because the filtering is later done by Model::_filterResults(). + * + * @param array &$resultSet Reference of resultset to be filtered. + * @param Model $Model Instance of model to operate against. + * @param array $filtered List of classes already filtered, to be skipped. + * @return array Array of results that have been filtered through $Model->afterFind. + */ + protected function _filterResults(&$resultSet, Model $Model, $filtered = []) + { + if (!is_array($resultSet)) { + return []; + } + + $current = reset($resultSet); + if (!is_array($current)) { + return []; + } + + $keys = array_diff(array_keys($current), $filtered, [$Model->alias]); + $filtering = []; + + foreach ($keys as $className) { + if (!isset($Model->{$className}) || !is_object($Model->{$className})) { + continue; + } + + $LinkedModel = $Model->{$className}; + $filtering[] = $className; + + foreach ($resultSet as $key => &$result) { + $data = $LinkedModel->afterFind([[$className => $result[$className]]], false); + if (isset($data[0][$className])) { + $result[$className] = $data[0][$className]; + } else { + unset($resultSet[$key]); + } + } + } + + return $filtering; + } + + /** + * Merge the results of 'hasMany' associations. + * + * Note: this function also deals with the formatting of the data. + * + * @param array &$resultSet Data to merge into. + * @param array $assocResultSet Data to merge. + * @param string $association Name of Model being merged. + * @param Model $Model Model being merged onto. + * @return void + */ + protected function _mergeHasMany(&$resultSet, $assocResultSet, $association, Model $Model) + { + $modelAlias = $Model->alias; + $primaryKey = $Model->primaryKey; + $foreignKey = $Model->hasMany[$association]['foreignKey']; + + // Make one pass through children and collect by parent key + // Make second pass through parents and associate children + $mergedByFK = []; + if (is_array($assocResultSet)) { + foreach ($assocResultSet as $data) { + $fk = $data[$association][$foreignKey]; + if (!array_key_exists($fk, $mergedByFK)) { + $mergedByFK[$fk] = []; + } + if (count($data) > 1) { + $data = array_merge($data[$association], $data); + unset($data[$association]); + foreach ($data as $key => $name) { + if (is_numeric($key)) { + $data[$association][] = $name; + unset($data[$key]); + } + } + $mergedByFK[$fk][] = $data; + } else { + $mergedByFK[$fk][] = $data[$association]; + } + } + } + + foreach ($resultSet as &$result) { + if (!isset($result[$modelAlias])) { + continue; + } + $merged = []; + $pk = $result[$modelAlias][$primaryKey]; + if (isset($mergedByFK[$pk])) { + $merged = $mergedByFK[$pk]; + } + $result = Hash::mergeDiff($result, [$association => $merged]); + } + } + + /** + * Fetch 'hasAndBelongsToMany' associations. + * + * @param Model $Model Primary model object. + * @param string $query Association query. + * @param array $ids Array of IDs of associated records. + * @param string $association Association name. + * @return array Association results. + */ + protected function _fetchHasAndBelongsToMany(Model $Model, $query, $ids, $association) + { + $ids = array_unique($ids); + + if (count($ids) > 1) { + $query = str_replace('{$__cakeID__$}', '(' . implode(', ', $ids) . ')', $query); + $query = str_replace('= (', 'IN (', $query); + } else { + $query = str_replace('{$__cakeID__$}', $ids[0], $query); + } + $query = str_replace(' WHERE 1 = 1', '', $query); + + return $this->fetchAll($query, $Model->cacheQueries); + } + + /** + * Merge association of merge into data + * + * @param array &$data The data to merge. + * @param array &$merge The data to merge. + * @param string $association The association name to merge. + * @param string $type The type of association + * @param bool $selfJoin Whether or not this is a self join. + * @return void + */ + protected function _mergeAssociation(&$data, &$merge, $association, $type, $selfJoin = false) + { + if (isset($merge[0]) && !isset($merge[0][$association])) { + $association = Inflector::pluralize($association); + } + + $dataAssociation =& $data[$association]; + + if ($type === 'belongsTo' || $type === 'hasOne') { + if (isset($merge[$association])) { + $dataAssociation = $merge[$association][0]; + } else { + if (!empty($merge[0][$association])) { + foreach ($merge[0] as $assoc => $data2) { + if ($assoc !== $association) { + $merge[0][$association][$assoc] = $data2; + } + } + } + if (!isset($dataAssociation)) { + $dataAssociation = []; + if ($merge[0][$association]) { + $dataAssociation = $merge[0][$association]; + } + } else { + if (is_array($merge[0][$association])) { + $mergeAssocTmp = []; + foreach ($dataAssociation as $k => $v) { + if (!is_array($v)) { + $dataAssocTmp[$k] = $v; + } + } + + foreach ($merge[0][$association] as $k => $v) { + if (!is_array($v)) { + $mergeAssocTmp[$k] = $v; + } + } + $dataKeys = array_keys($data); + $mergeKeys = array_keys($merge[0]); + + if ($mergeKeys[0] === $dataKeys[0] || $mergeKeys === $dataKeys) { + $dataAssociation[$association] = $merge[0][$association]; + } else { + $diff = Hash::diff($dataAssocTmp, $mergeAssocTmp); + $dataAssociation = array_merge($merge[0][$association], $diff); + } + } else if ($selfJoin && array_key_exists($association, $merge[0])) { + $dataAssociation = array_merge($dataAssociation, [$association => []]); + } + } + } + } else { + if (isset($merge[0][$association]) && $merge[0][$association] === false) { + if (!isset($dataAssociation)) { + $dataAssociation = []; + } + } else { + foreach ($merge as $row) { + $insert = []; + if (count($row) === 1) { + $insert = $row[$association]; + } else if (isset($row[$association])) { + $insert = array_merge($row[$association], $row); + unset($insert[$association]); + } + + if (empty($dataAssociation) || (isset($dataAssociation) && !in_array($insert, $dataAssociation, true))) { + $dataAssociation[] = $insert; + } + } + } + } + } + + /** + * Fetch 'hasMany' associations. + * + * This is just a proxy to maintain BC. + * + * @param Model $Model Primary model object. + * @param string $query Association query template. + * @param array $ids Array of IDs of associated records. + * @return array Association results. + * @see DboSource::_fetchHasMany() + */ + public function fetchAssociated(Model $Model, $query, $ids) + { + return $this->_fetchHasMany($Model, $query, $ids); + } + + /** + * Generates and executes an SQL UPDATE statement for given model, fields, and values. + * For databases that do not support aliases in UPDATE queries. + * + * @param Model $Model The model to update. + * @param array $fields The fields to update + * @param array $values The values fo the fields. + * @param mixed $conditions The conditions for the update. When non-empty $values will not be quoted. + * @return bool Success + */ + public function update(Model $Model, $fields = [], $values = null, $conditions = null) + { + if (!$values) { + $combined = $fields; + } else { + $combined = array_combine($fields, $values); + } + + $fields = implode(', ', $this->_prepareUpdateFields($Model, $combined, empty($conditions))); + + $alias = $joins = null; + $table = $this->fullTableName($Model); + $conditions = $this->_matchRecords($Model, $conditions); + + if ($conditions === false) { + return false; + } + $query = compact('table', 'alias', 'joins', 'fields', 'conditions'); + + if (!$this->execute($this->renderStatement('update', $query))) { + $Model->onError(); + return false; + } + return true; + } + + /** + * Quotes and prepares fields and values for an SQL UPDATE statement + * + * @param Model $Model The model to prepare fields for. + * @param array $fields The fields to update. + * @param bool $quoteValues If values should be quoted, or treated as SQL snippets + * @param bool $alias Include the model alias in the field name + * @return array Fields and values, quoted and prepared + */ + protected function _prepareUpdateFields(Model $Model, $fields, $quoteValues = true, $alias = false) + { + $quotedAlias = $this->startQuote . $Model->alias . $this->endQuote; + $schema = $Model->schema(); + + $updates = []; + foreach ($fields as $field => $value) { + if ($alias && strpos($field, '.') === false) { + $quoted = $Model->escapeField($field); + } else if (!$alias && strpos($field, '.') !== false) { + $quoted = $this->name(str_replace($quotedAlias . '.', '', str_replace( + $Model->alias . '.', '', $field + ))); + } else { + $quoted = $this->name($field); + } + + if ($value === null) { + $updates[] = $quoted . ' = NULL'; + continue; + } + $update = $quoted . ' = '; + + if ($quoteValues) { + $update .= $this->value($value, $Model->getColumnType($field), isset($schema[$field]['null']) ? $schema[$field]['null'] : true); + } else if ($Model->getColumnType($field) === 'boolean' && (is_int($value) || is_bool($value))) { + $update .= $this->boolean($value, true); + } else if (!$alias) { + $update .= str_replace($quotedAlias . '.', '', str_replace( + $Model->alias . '.', '', $value + )); + } else { + $update .= $value; + } + $updates[] = $update; + } + return $updates; + } + + /** + * Gets a list of record IDs for the given conditions. Used for multi-record updates and deletes + * in databases that do not support aliases in UPDATE/DELETE queries. + * + * @param Model $Model The model to find matching records for. + * @param mixed $conditions The conditions to match against. + * @return array List of record IDs + */ + protected function _matchRecords(Model $Model, $conditions = null) + { + if ($conditions === true) { + $conditions = $this->conditions(true); + } else if ($conditions === null) { + $conditions = $this->conditions($this->defaultConditions($Model, $conditions, false), true, true, $Model); + } else { + $noJoin = true; + foreach ($conditions as $field => $value) { + $originalField = $field; + if (strpos($field, '.') !== false) { + list(, $field) = explode('.', $field); + $field = ltrim($field, $this->startQuote); + $field = rtrim($field, $this->endQuote); + } + if (!$Model->hasField($field)) { + $noJoin = false; + break; + } + if ($field !== $originalField) { + $conditions[$field] = $value; + unset($conditions[$originalField]); + } + } + if ($noJoin === true) { + return $this->conditions($conditions); + } + $idList = $Model->find('all', [ + 'fields' => "{$Model->alias}.{$Model->primaryKey}", + 'conditions' => $conditions + ]); + + if (empty($idList)) { + return false; + } + + $conditions = $this->conditions([ + $Model->primaryKey => Hash::extract($idList, "{n}.{$Model->alias}.{$Model->primaryKey}") + ]); + } + + return $conditions; + } + + /** + * Creates a default set of conditions from the model if $conditions is null/empty. + * If conditions are supplied then they will be returned. If a model doesn't exist and no conditions + * were provided either null or false will be returned based on what was input. + * + * @param Model $Model The model to get conditions for. + * @param string|array|bool $conditions Array of conditions, conditions string, null or false. If an array of conditions, + * or string conditions those conditions will be returned. With other values the model's existence will be checked. + * If the model doesn't exist a null or false will be returned depending on the input value. + * @param bool $useAlias Use model aliases rather than table names when generating conditions + * @return mixed Either null, false, $conditions or an array of default conditions to use. + * @see DboSource::update() + * @see DboSource::conditions() + */ + public function defaultConditions(Model $Model, $conditions, $useAlias = true) + { + if (!empty($conditions)) { + return $conditions; + } + $exists = $Model->exists($Model->getID()); + if (!$exists && ($conditions !== null || !empty($Model->__safeUpdateMode))) { + return false; + } else if (!$exists) { + return null; + } + $alias = $Model->alias; + + if (!$useAlias) { + $alias = $this->fullTableName($Model, false); + } + return ["{$alias}.{$Model->primaryKey}" => $Model->getID()]; + } + + /** + * Generates and executes an SQL DELETE statement. + * For databases that do not support aliases in UPDATE queries. + * + * @param Model $Model The model to delete from + * @param mixed $conditions The conditions to use. If empty the model's primary key will be used. + * @return bool Success + */ + public function delete(Model $Model, $conditions = null) + { + $alias = $joins = null; + $table = $this->fullTableName($Model); + $conditions = $this->_matchRecords($Model, $conditions); + + if ($conditions === false) { + return false; + } + + if ($this->execute($this->renderStatement('delete', compact('alias', 'table', 'joins', 'conditions'))) === false) { + $Model->onError(); + return false; + } + return true; + } + + /** + * Returns an SQL calculation, i.e. COUNT() or MAX() + * + * @param Model $Model The model to get a calculated field for. + * @param string $func Lowercase name of SQL function, i.e. 'count' or 'max' + * @param array $params Function parameters (any values must be quoted manually) + * @return string An SQL calculation function + */ + public function calculate(Model $Model, $func, $params = []) + { + $params = (array)$params; + + switch (strtolower($func)) { + case 'count': + if (!isset($params[0])) { + $params[0] = '*'; + } + if (!isset($params[1])) { + $params[1] = 'count'; + } + if ($Model->isVirtualField($params[0])) { + $arg = $this->_quoteFields($Model->getVirtualField($params[0])); + } else { + $arg = $this->name($params[0]); + } + return 'COUNT(' . $arg . ') AS ' . $this->name($params[1]); + case 'max': + case 'min': + if (!isset($params[1])) { + $params[1] = $params[0]; + } + if ($Model->isVirtualField($params[0])) { + $arg = $this->_quoteFields($Model->getVirtualField($params[0])); + } else { + $arg = $this->name($params[0]); + } + return strtoupper($func) . '(' . $arg . ') AS ' . $this->name($params[1]); + } + } + + /** + * Deletes all the records in a table and resets the count of the auto-incrementing + * primary key, where applicable. + * + * @param Model|string $table A string or model class representing the table to be truncated + * @return bool SQL TRUNCATE TABLE statement, false if not applicable. + */ + public function truncate($table) + { + return $this->execute('TRUNCATE TABLE ' . $this->fullTableName($table)); + } + + /** + * Rollback a transaction + * + * @return bool True on success, false on fail + * (i.e. if the database/model does not support transactions, + * or a transaction has not started). + */ + public function rollback() + { + if (!$this->_transactionStarted) { + return false; + } + + if ($this->_transactionNesting === 0) { + if ($this->fullDebug) { + $this->took = $this->numRows = $this->affected = false; + $this->logQuery('ROLLBACK'); + } + $this->_transactionStarted = false; + return $this->_connection->rollBack(); + } + + if ($this->nestedTransactionSupported()) { + return $this->_rollbackNested(); + } + + $this->_transactionNesting--; + return true; + } + + /** + * Check if the server support nested transactions + * + * @return bool + */ + public function nestedTransactionSupported() + { + return false; + } + + /** + * Rollback a nested transaction + * + * @return bool + */ + protected function _rollbackNested() + { + $query = 'ROLLBACK TO SAVEPOINT LEVEL' . $this->_transactionNesting--; + if ($this->fullDebug) { + $this->took = $this->numRows = $this->affected = false; + $this->logQuery($query); + } + $this->_connection->exec($query); + return true; + } + + /** + * Returns a key formatted like a string Model.fieldname(i.e. Post.title, or Country.name) + * + * @param Model $Model The model to get a key for. + * @param string $key The key field. + * @param string $assoc The association name. + * @return string + */ + public function resolveKey(Model $Model, $key, $assoc = null) + { + if (strpos('.', $key) !== false) { + return $this->name($Model->alias) . '.' . $this->name($key); + } + return $key; + } + + /** + * Disconnects database, kills the connection and says the connection is closed. + * + * @return void + */ + public function close() + { + $this->disconnect(); + } + + /** + * Checks if the specified table contains any record matching specified SQL + * + * @param Model $Model Model to search + * @param string $sql SQL WHERE clause (condition only, not the "WHERE" part) + * @return bool True if the table has a matching record, else false + */ + public function hasAny(Model $Model, $sql) + { + $sql = $this->conditions($sql); + $table = $this->fullTableName($Model); + $alias = $this->alias . $this->name($Model->alias); + $where = $sql ? "{$sql}" : ' WHERE 1 = 1'; + $id = $Model->escapeField(); + + $out = $this->fetchRow("SELECT COUNT({$id}) {$this->alias}count FROM {$table} {$alias}{$where}"); + + if (is_array($out)) { + return $out[0]['count']; + } + return false; + } + + /** + * Gets the length of a database-native column description, or null if no length + * + * @param string $real Real database-layer column type (i.e. "varchar(255)") + * @return mixed An integer or string representing the length of the column, or null for unknown length. + */ + public function length($real) + { + preg_match('/([\w\s]+)(?:\((.+?)\))?(\sunsigned)?/i', $real, $result); + $types = [ + 'int' => 1, 'tinyint' => 1, 'smallint' => 1, 'mediumint' => 1, 'integer' => 1, 'bigint' => 1 + ]; + + $type = $length = null; + if (isset($result[1])) { + $type = $result[1]; + } + if (isset($result[2])) { + $length = $result[2]; + } + $sign = isset($result[3]); + + $isFloat = in_array($type, ['dec', 'decimal', 'float', 'numeric', 'double']); + if ($isFloat && strpos($length, ',') !== false) { + return $length; + } + + if ($length === null) { + return null; + } + + if (isset($types[$type])) { + return (int)$length; + } + if (in_array($type, ['enum', 'set'])) { + return null; + } + return (int)$length; + } + + /** + * Inserts multiple values into a table + * + * @param string $table The table being inserted into. + * @param array $fields The array of field/column names being inserted. + * @param array $values The array of values to insert. The values should + * be an array of rows. Each row should have values keyed by the column name. + * Each row must have the values in the same order as $fields. + * @return bool + */ + public function insertMulti($table, $fields, $values) + { + $table = $this->fullTableName($table); + $holder = implode(',', array_fill(0, count($fields), '?')); + $fields = implode(', ', array_map([&$this, 'name'], $fields)); + + $pdoMap = [ + 'integer' => PDO::PARAM_INT, + 'float' => PDO::PARAM_STR, + 'boolean' => PDO::PARAM_BOOL, + 'string' => PDO::PARAM_STR, + 'text' => PDO::PARAM_STR + ]; + $columnMap = []; + + $sql = "INSERT INTO {$table} ({$fields}) VALUES ({$holder})"; + $statement = $this->_connection->prepare($sql); + $this->begin(); + + foreach ($values[key($values)] as $key => $val) { + $type = $this->introspectType($val); + $columnMap[$key] = $pdoMap[$type]; + } + + foreach ($values as $value) { + $i = 1; + foreach ($value as $col => $val) { + $statement->bindValue($i, $val, $columnMap[$col]); + $i += 1; + } + $t = microtime(true); + $statement->execute(); + $statement->closeCursor(); + + if ($this->fullDebug) { + $this->took = round((microtime(true) - $t) * 1000, 0); + $this->numRows = $this->affected = $statement->rowCount(); + $this->logQuery($sql, $value); + } + } + return $this->commit(); + } + + /** + * Begin a transaction + * + * @return bool True on success, false on fail + * (i.e. if the database/model does not support transactions, + * or a transaction has not started). + */ + public function begin() + { + if ($this->_transactionStarted) { + if ($this->nestedTransactionSupported()) { + return $this->_beginNested(); + } + $this->_transactionNesting++; + return $this->_transactionStarted; + } + + $this->_transactionNesting = 0; + if ($this->fullDebug) { + $this->took = $this->numRows = $this->affected = false; + $this->logQuery('BEGIN'); + } + return $this->_transactionStarted = $this->_connection->beginTransaction(); + } + + /** + * Begin a nested transaction + * + * @return bool + */ + protected function _beginNested() + { + $query = 'SAVEPOINT LEVEL' . ++$this->_transactionNesting; + if ($this->fullDebug) { + $this->took = $this->numRows = $this->affected = false; + $this->logQuery($query); + } + $this->_connection->exec($query); + return true; + } + + /** + * Commit a transaction + * + * @return bool True on success, false on fail + * (i.e. if the database/model does not support transactions, + * or a transaction has not started). + */ + public function commit() + { + if (!$this->_transactionStarted) { + return false; + } + + if ($this->_transactionNesting === 0) { + if ($this->fullDebug) { + $this->took = $this->numRows = $this->affected = false; + $this->logQuery('COMMIT'); + } + $this->_transactionStarted = false; + return $this->_connection->commit(); + } + + if ($this->nestedTransactionSupported()) { + return $this->_commitNested(); + } + + $this->_transactionNesting--; + return true; + } + + /** + * Commit a nested transaction + * + * @return bool + */ + protected function _commitNested() + { + $query = 'RELEASE SAVEPOINT LEVEL' . $this->_transactionNesting--; + if ($this->fullDebug) { + $this->took = $this->numRows = $this->affected = false; + $this->logQuery($query); + } + $this->_connection->exec($query); + return true; + } + + /** + * Reset a sequence based on the MAX() value of $column. Useful + * for resetting sequences after using insertMulti(). + * + * This method should be implemented by datasources that require sequences to be used. + * + * @param string $table The name of the table to update. + * @param string $column The column to use when resetting the sequence value. + * @return bool Success. + */ + public function resetSequence($table, $column) + { + } + + /** + * Returns an array of the indexes in given datasource name. + * + * @param string $model Name of model to inspect + * @return array Fields in table. Keys are column and unique + */ + public function index($model) + { + return []; + } + + /** + * Generate a database-native schema for the given Schema object + * + * @param CakeSchema $schema An instance of a subclass of CakeSchema + * @param string $tableName Optional. If specified only the table name given will be generated. + * Otherwise, all tables defined in the schema are generated. + * @return string + */ + public function createSchema($schema, $tableName = null) + { + if (!$schema instanceof CakeSchema) { + trigger_error(__d('cake_dev', 'Invalid schema object'), E_USER_WARNING); + return null; + } + $out = ''; + + foreach ($schema->tables as $curTable => $columns) { + if (!$tableName || $tableName === $curTable) { + $cols = $indexes = $tableParameters = []; + $primary = null; + $table = $this->fullTableName($curTable); + + $primaryCount = 0; + foreach ($columns as $col) { + if (isset($col['key']) && $col['key'] === 'primary') { + $primaryCount++; + } + } + + foreach ($columns as $name => $col) { + if (is_string($col)) { + $col = ['type' => $col]; + } + $isPrimary = isset($col['key']) && $col['key'] === 'primary'; + // Multi-column primary keys are not supported. + if ($isPrimary && $primaryCount > 1) { + unset($col['key']); + $isPrimary = false; + } + if ($isPrimary) { + $primary = $name; + } + if ($name !== 'indexes' && $name !== 'tableParameters') { + $col['name'] = $name; + if (!isset($col['type'])) { + $col['type'] = 'string'; + } + $cols[] = $this->buildColumn($col); + } else if ($name === 'indexes') { + $indexes = array_merge($indexes, $this->buildIndex($col, $table)); + } else if ($name === 'tableParameters') { + $tableParameters = array_merge($tableParameters, $this->buildTableParameters($col, $table)); + } + } + if (!isset($columns['indexes']['PRIMARY']) && !empty($primary)) { + $col = ['PRIMARY' => ['column' => $primary, 'unique' => 1]]; + $indexes = array_merge($indexes, $this->buildIndex($col, $table)); + } + $columns = $cols; + $out .= $this->renderStatement('schema', compact('table', 'columns', 'indexes', 'tableParameters')) . "\n\n"; + } + } + return $out; + } + + /** + * Generate a database-native column schema string + * + * @param array $column An array structured like the following: array('name' => 'value', 'type' => 'value'[, options]), + * where options can be 'default', 'length', or 'key'. + * @return string + */ + public function buildColumn($column) + { + $name = $type = null; + extract(array_merge(['null' => true], $column)); + + if (empty($name) || empty($type)) { + trigger_error(__d('cake_dev', 'Column name or type not defined in schema'), E_USER_WARNING); + return null; + } + + if (!isset($this->columns[$type]) && substr($type, 0, 4) !== 'enum') { + trigger_error(__d('cake_dev', 'Column type %s does not exist', $type), E_USER_WARNING); + return null; + } + + if (substr($type, 0, 4) === 'enum') { + $out = $this->name($name) . ' ' . $type; + } else { + $real = $this->columns[$type]; + $out = $this->name($name) . ' ' . $real['name']; + if (isset($column['length'])) { + $length = $column['length']; + } else if (isset($column['limit'])) { + $length = $column['limit']; + } else if (isset($real['length'])) { + $length = $real['length']; + } else if (isset($real['limit'])) { + $length = $real['limit']; + } + if (isset($length)) { + $out .= '(' . $length . ')'; + } + } + + if (($column['type'] === 'integer' || $column['type'] === 'float') && isset($column['default']) && $column['default'] === '') { + $column['default'] = null; + } + $out = $this->_buildFieldParameters($out, $column, 'beforeDefault'); + + if (isset($column['key']) && $column['key'] === 'primary' && ($type === 'integer' || $type === 'biginteger')) { + $out .= ' ' . $this->columns['primary_key']['name']; + } else if (isset($column['key']) && $column['key'] === 'primary') { + $out .= ' NOT NULL'; + } else if (isset($column['default']) && isset($column['null']) && $column['null'] === false) { + $out .= ' DEFAULT ' . $this->value($column['default'], $type) . ' NOT NULL'; + } else if (isset($column['default'])) { + $out .= ' DEFAULT ' . $this->value($column['default'], $type); + } else if ($type !== 'timestamp' && !empty($column['null'])) { + $out .= ' DEFAULT NULL'; + } else if ($type === 'timestamp' && !empty($column['null'])) { + $out .= ' NULL'; + } else if (isset($column['null']) && $column['null'] === false) { + $out .= ' NOT NULL'; + } + if (in_array($type, ['timestamp', 'datetime']) && isset($column['default']) && strtolower($column['default']) === 'current_timestamp') { + $out = str_replace(["'CURRENT_TIMESTAMP'", "'current_timestamp'"], 'CURRENT_TIMESTAMP', $out); + } + return $this->_buildFieldParameters($out, $column, 'afterDefault'); + } + + /** + * Build the field parameters, in a position + * + * @param string $columnString The partially built column string + * @param array $columnData The array of column data. + * @param string $position The position type to use. 'beforeDefault' or 'afterDefault' are common + * @return string a built column with the field parameters added. + */ + protected function _buildFieldParameters($columnString, $columnData, $position) + { + foreach ($this->fieldParameters as $paramName => $value) { + if (isset($columnData[$paramName]) && $value['position'] == $position) { + if (isset($value['options']) && !in_array($columnData[$paramName], $value['options'], true)) { + continue; + } + if (isset($value['types']) && !in_array($columnData['type'], $value['types'], true)) { + continue; + } + $val = $columnData[$paramName]; + if ($value['quote']) { + $val = $this->value($val); + } + $columnString .= ' ' . $value['value'] . (empty($value['noVal']) ? $value['join'] . $val : ''); + } + } + return $columnString; + } + + /** + * Format indexes for create table. + * + * @param array $indexes The indexes to build + * @param string $table The table name. + * @return array + */ + public function buildIndex($indexes, $table = null) + { + $join = []; + foreach ($indexes as $name => $value) { + $out = ''; + if ($name === 'PRIMARY') { + $out .= 'PRIMARY '; + $name = null; + } else { + if (!empty($value['unique'])) { + $out .= 'UNIQUE '; + } + $name = $this->startQuote . $name . $this->endQuote; + } + if (is_array($value['column'])) { + $out .= 'KEY ' . $name . ' (' . implode(', ', array_map([&$this, 'name'], $value['column'])) . ')'; + } else { + $out .= 'KEY ' . $name . ' (' . $this->name($value['column']) . ')'; + } + $join[] = $out; + } + return $join; + } + + /** + * Format parameters for create table + * + * @param array $parameters The parameters to create SQL for. + * @param string $table The table name. + * @return array + */ + public function buildTableParameters($parameters, $table = null) + { + $result = []; + foreach ($parameters as $name => $value) { + if (isset($this->tableParameters[$name])) { + if ($this->tableParameters[$name]['quote']) { + $value = $this->value($value); + } + $result[] = $this->tableParameters[$name]['value'] . $this->tableParameters[$name]['join'] . $value; + } + } + return $result; + } + + /** + * Generate an alter syntax from CakeSchema::compare() + * + * @param mixed $compare The comparison data. + * @param string $table The table name. + * @return bool + */ + public function alterSchema($compare, $table = null) + { + return false; + } + + /** + * Generate a "drop table" statement for the given Schema object + * + * @param CakeSchema $schema An instance of a subclass of CakeSchema + * @param string $table Optional. If specified only the table name given will be generated. + * Otherwise, all tables defined in the schema are generated. + * @return string + */ + public function dropSchema(CakeSchema $schema, $table = null) + { + $out = ''; + + if ($table && array_key_exists($table, $schema->tables)) { + return $this->_dropTable($table) . "\n"; + } else if ($table) { + return $out; + } + + foreach (array_keys($schema->tables) as $curTable) { + $out .= $this->_dropTable($curTable) . "\n"; + } + return $out; + } + + /** + * Generate a "drop table" statement for a single table + * + * @param type $table Name of the table to drop + * @return string Drop table SQL statement + */ + protected function _dropTable($table) + { + return 'DROP TABLE ' . $this->fullTableName($table) . ";"; + } + + /** + * Read additional table parameters + * + * @param string $name The table name to read. + * @return array + */ + public function readTableParameters($name) + { + $parameters = []; + if (method_exists($this, 'listDetailedSources')) { + $currentTableDetails = $this->listDetailedSources($name); + foreach ($this->tableParameters as $paramName => $parameter) { + if (!empty($parameter['column']) && !empty($currentTableDetails[$parameter['column']])) { + $parameters[$paramName] = $currentTableDetails[$parameter['column']]; + } + } + } + return $parameters; + } + + /** + * Empties the query caches. + * + * @return void + */ + public function flushQueryCache() + { + $this->_queryCache = []; + } + + /** + * Used for storing in cache the results of the in-memory methodCache + */ + public function __destruct() + { + if ($this->_methodCacheChange) { + Cache::write('method_cache', static::$methodCache, '_cake_core_'); + } + parent::__destruct(); + } + + /** + * Returns an array of SQL JOIN conditions from a model's associations. + * + * @param Model $Model The model to get joins for.2 + * @return array + */ + protected function _getJoins(Model $Model) + { + $join = []; + $joins = array_merge($Model->getAssociated('hasOne'), $Model->getAssociated('belongsTo')); + + foreach ($joins as $assoc) { + if (!isset($Model->{$assoc})) { + continue; + } + + $LinkModel = $Model->{$assoc}; + + if ($Model->useDbConfig !== $LinkModel->useDbConfig) { + continue; + } + + $assocData = $Model->getAssociated($assoc); + + $join[] = $this->buildJoinStatement([ + 'table' => $LinkModel, + 'alias' => $assoc, + 'type' => isset($assocData['type']) ? $assocData['type'] : 'LEFT', + 'conditions' => trim($this->conditions( + $this->_mergeConditions($assocData['conditions'], $this->getConstraint($assocData['association'], $Model, $LinkModel, $assoc, $assocData)), + true, + false, + $Model + )) + ]); + } + + return $join; + } + + /** + * Auxiliary function to quote matches `Model.fields` from a preg_replace_callback call + * + * @param string $match matched string + * @return string quoted string + */ + protected function _quoteMatchedField($match) + { + if (is_numeric($match[0])) { + return $match[0]; + } + return $this->name($match[0]); + } } diff --git a/lib/Cake/Model/Datasource/Session/CacheSession.php b/lib/Cake/Model/Datasource/Session/CacheSession.php index 5d639edf..db58060c 100755 --- a/lib/Cake/Model/Datasource/Session/CacheSession.php +++ b/lib/Cake/Model/Datasource/Session/CacheSession.php @@ -25,70 +25,77 @@ * @package Cake.Model.Datasource.Session * @see CakeSession for configuration information. */ -class CacheSession implements CakeSessionHandlerInterface { +class CacheSession implements CakeSessionHandlerInterface +{ -/** - * Method called on open of a database session. - * - * @return bool Success - */ - public function open() { - return true; - } + /** + * Method called on open of a database session. + * + * @return bool Success + */ + public function open() + { + return true; + } -/** - * Method called on close of a database session. - * - * @return bool Success - */ - public function close() { - return true; - } + /** + * Method called on close of a database session. + * + * @return bool Success + */ + public function close() + { + return true; + } -/** - * Method used to read from a database session. - * - * @param string $id The key of the value to read - * @return mixed The value of the key or false if it does not exist - */ - public function read($id) { - $data = Cache::read($id, Configure::read('Session.handler.config')); + /** + * Method used to read from a database session. + * + * @param string $id The key of the value to read + * @return mixed The value of the key or false if it does not exist + */ + public function read($id) + { + $data = Cache::read($id, Configure::read('Session.handler.config')); - if (!is_numeric($data) && empty($data)) { - return ''; - } - return $data; - } + if (!is_numeric($data) && empty($data)) { + return ''; + } + return $data; + } -/** - * Helper function called on write for database sessions. - * - * @param int $id ID that uniquely identifies session in database - * @param mixed $data The value of the data to be saved. - * @return bool True for successful write, false otherwise. - */ - public function write($id, $data) { - return (bool)Cache::write($id, $data, Configure::read('Session.handler.config')); - } + /** + * Helper function called on write for database sessions. + * + * @param int $id ID that uniquely identifies session in database + * @param mixed $data The value of the data to be saved. + * @return bool True for successful write, false otherwise. + */ + public function write($id, $data) + { + return (bool)Cache::write($id, $data, Configure::read('Session.handler.config')); + } -/** - * Method called on the destruction of a database session. - * - * @param int $id ID that uniquely identifies session in cache - * @return bool True for successful delete, false otherwise. - */ - public function destroy($id) { - return (bool)Cache::delete($id, Configure::read('Session.handler.config')); - } + /** + * Method called on the destruction of a database session. + * + * @param int $id ID that uniquely identifies session in cache + * @return bool True for successful delete, false otherwise. + */ + public function destroy($id) + { + return (bool)Cache::delete($id, Configure::read('Session.handler.config')); + } -/** - * Helper function called on gc for cache sessions. - * - * @param int $expires Timestamp (defaults to current time) - * @return bool Success - */ - public function gc($expires = null) { - return (bool)Cache::gc(Configure::read('Session.handler.config'), $expires); - } + /** + * Helper function called on gc for cache sessions. + * + * @param int $expires Timestamp (defaults to current time) + * @return bool Success + */ + public function gc($expires = null) + { + return (bool)Cache::gc(Configure::read('Session.handler.config'), $expires); + } } diff --git a/lib/Cake/Model/Datasource/Session/CakeSessionHandlerInterface.php b/lib/Cake/Model/Datasource/Session/CakeSessionHandlerInterface.php index be7d1efc..0fccd821 100755 --- a/lib/Cake/Model/Datasource/Session/CakeSessionHandlerInterface.php +++ b/lib/Cake/Model/Datasource/Session/CakeSessionHandlerInterface.php @@ -20,54 +20,55 @@ * * @package Cake.Model.Datasource.Session */ -interface CakeSessionHandlerInterface { +interface CakeSessionHandlerInterface +{ -/** - * Method called on open of a session. - * - * @return bool Success - */ - public function open(); + /** + * Method called on open of a session. + * + * @return bool Success + */ + public function open(); -/** - * Method called on close of a session. - * - * @return bool Success - */ - public function close(); + /** + * Method called on close of a session. + * + * @return bool Success + */ + public function close(); -/** - * Method used to read from a session. - * - * @param string $id The key of the value to read - * @return mixed The value of the key or false if it does not exist - */ - public function read($id); + /** + * Method used to read from a session. + * + * @param string $id The key of the value to read + * @return mixed The value of the key or false if it does not exist + */ + public function read($id); -/** - * Helper function called on write for sessions. - * - * @param int $id ID that uniquely identifies session in database - * @param mixed $data The value of the data to be saved. - * @return bool True for successful write, false otherwise. - */ - public function write($id, $data); + /** + * Helper function called on write for sessions. + * + * @param int $id ID that uniquely identifies session in database + * @param mixed $data The value of the data to be saved. + * @return bool True for successful write, false otherwise. + */ + public function write($id, $data); -/** - * Method called on the destruction of a session. - * - * @param int $id ID that uniquely identifies session in database - * @return bool True for successful delete, false otherwise. - */ - public function destroy($id); + /** + * Method called on the destruction of a session. + * + * @param int $id ID that uniquely identifies session in database + * @return bool True for successful delete, false otherwise. + */ + public function destroy($id); -/** - * Run the Garbage collection on the session storage. This method should vacuum all - * expired or dead sessions. - * - * @param int $expires Timestamp (defaults to current time) - * @return bool Success - */ - public function gc($expires = null); + /** + * Run the Garbage collection on the session storage. This method should vacuum all + * expired or dead sessions. + * + * @param int $expires Timestamp (defaults to current time) + * @return bool Success + */ + public function gc($expires = null); } diff --git a/lib/Cake/Model/Datasource/Session/DatabaseSession.php b/lib/Cake/Model/Datasource/Session/DatabaseSession.php index c77f3de1..1c0fab3d 100755 --- a/lib/Cake/Model/Datasource/Session/DatabaseSession.php +++ b/lib/Cake/Model/Datasource/Session/DatabaseSession.php @@ -24,139 +24,147 @@ * * @package Cake.Model.Datasource.Session */ -class DatabaseSession implements CakeSessionHandlerInterface { - -/** - * Reference to the model handling the session data - * - * @var Model - */ - protected $_model; - -/** - * Number of seconds to mark the session as expired - * - * @var int - */ - protected $_timeout; - -/** - * Constructor. Looks at Session configuration information and - * sets up the session model. - */ - public function __construct() { - $modelName = Configure::read('Session.handler.model'); - - if (empty($modelName)) { - $settings = array( - 'class' => 'Session', - 'alias' => 'Session', - 'table' => 'cake_sessions', - ); - } else { - $settings = array( - 'class' => $modelName, - 'alias' => 'Session', - ); - } - $this->_model = ClassRegistry::init($settings); - $this->_timeout = Configure::read('Session.timeout') * 60; - } - -/** - * Method called on open of a database session. - * - * @return bool Success - */ - public function open() { - return true; - } - -/** - * Method called on close of a database session. - * - * @return bool Success - */ - public function close() { - return true; - } - -/** - * Method used to read from a database session. - * - * @param int|string $id The key of the value to read - * @return mixed The value of the key or false if it does not exist - */ - public function read($id) { - $row = $this->_model->find('first', array( - 'conditions' => array($this->_model->alias . '.' . $this->_model->primaryKey => $id) - )); - - if (empty($row[$this->_model->alias])) { - return ''; - } - - if (!is_numeric($row[$this->_model->alias]['data']) && empty($row[$this->_model->alias]['data'])) { - return ''; - } - - return (string)$row[$this->_model->alias]['data']; - } - -/** - * Helper function called on write for database sessions. - * - * Will retry, once, if the save triggers a PDOException which - * can happen if a race condition is encountered - * - * @param int $id ID that uniquely identifies session in database - * @param mixed $data The value of the data to be saved. - * @return bool True for successful write, false otherwise. - */ - public function write($id, $data) { - if (!$id) { - return false; - } - $expires = time() + $this->_timeout; - $record = compact('id', 'data', 'expires'); - $record[$this->_model->primaryKey] = $id; - - $options = array( - 'validate' => false, - 'callbacks' => false, - 'counterCache' => false - ); - try { - return (bool)$this->_model->save($record, $options); - } catch (PDOException $e) { - return (bool)$this->_model->save($record, $options); - } - } - -/** - * Method called on the destruction of a database session. - * - * @param int $id ID that uniquely identifies session in database - * @return bool True for successful delete, false otherwise. - */ - public function destroy($id) { - return (bool)$this->_model->delete($id); - } - -/** - * Helper function called on gc for database sessions. - * - * @param int $expires Timestamp (defaults to current time) - * @return bool Success - */ - public function gc($expires = null) { - if (!$expires) { - $expires = time(); - } else { - $expires = time() - $expires; - } - $this->_model->deleteAll(array($this->_model->alias . ".expires <" => $expires), false, false); - return true; - } +class DatabaseSession implements CakeSessionHandlerInterface +{ + + /** + * Reference to the model handling the session data + * + * @var Model + */ + protected $_model; + + /** + * Number of seconds to mark the session as expired + * + * @var int + */ + protected $_timeout; + + /** + * Constructor. Looks at Session configuration information and + * sets up the session model. + */ + public function __construct() + { + $modelName = Configure::read('Session.handler.model'); + + if (empty($modelName)) { + $settings = [ + 'class' => 'Session', + 'alias' => 'Session', + 'table' => 'cake_sessions', + ]; + } else { + $settings = [ + 'class' => $modelName, + 'alias' => 'Session', + ]; + } + $this->_model = ClassRegistry::init($settings); + $this->_timeout = Configure::read('Session.timeout') * 60; + } + + /** + * Method called on open of a database session. + * + * @return bool Success + */ + public function open() + { + return true; + } + + /** + * Method called on close of a database session. + * + * @return bool Success + */ + public function close() + { + return true; + } + + /** + * Method used to read from a database session. + * + * @param int|string $id The key of the value to read + * @return mixed The value of the key or false if it does not exist + */ + public function read($id) + { + $row = $this->_model->find('first', [ + 'conditions' => [$this->_model->alias . '.' . $this->_model->primaryKey => $id] + ]); + + if (empty($row[$this->_model->alias])) { + return ''; + } + + if (!is_numeric($row[$this->_model->alias]['data']) && empty($row[$this->_model->alias]['data'])) { + return ''; + } + + return (string)$row[$this->_model->alias]['data']; + } + + /** + * Helper function called on write for database sessions. + * + * Will retry, once, if the save triggers a PDOException which + * can happen if a race condition is encountered + * + * @param int $id ID that uniquely identifies session in database + * @param mixed $data The value of the data to be saved. + * @return bool True for successful write, false otherwise. + */ + public function write($id, $data) + { + if (!$id) { + return false; + } + $expires = time() + $this->_timeout; + $record = compact('id', 'data', 'expires'); + $record[$this->_model->primaryKey] = $id; + + $options = [ + 'validate' => false, + 'callbacks' => false, + 'counterCache' => false + ]; + try { + return (bool)$this->_model->save($record, $options); + } catch (PDOException $e) { + return (bool)$this->_model->save($record, $options); + } + } + + /** + * Method called on the destruction of a database session. + * + * @param int $id ID that uniquely identifies session in database + * @return bool True for successful delete, false otherwise. + */ + public function destroy($id) + { + return (bool)$this->_model->delete($id); + } + + /** + * Helper function called on gc for database sessions. + * + * @param int $expires Timestamp (defaults to current time) + * @return bool Success + */ + public function gc($expires = null) + { + if (!$expires) { + $expires = time(); + } else { + $expires = time() - $expires; + } + $this->_model->deleteAll([$this->_model->alias . ".expires <" => $expires], false, false); + return true; + } } diff --git a/lib/Cake/Model/I18nModel.php b/lib/Cake/Model/I18nModel.php index 8ccf2af4..1448aa55 100755 --- a/lib/Cake/Model/I18nModel.php +++ b/lib/Cake/Model/I18nModel.php @@ -21,27 +21,28 @@ * * @package Cake.Model */ -class I18nModel extends AppModel { +class I18nModel extends AppModel +{ -/** - * Model name - * - * @var string - */ - public $name = 'I18nModel'; + /** + * Model name + * + * @var string + */ + public $name = 'I18nModel'; -/** - * Table name - * - * @var string - */ - public $useTable = 'i18n'; + /** + * Table name + * + * @var string + */ + public $useTable = 'i18n'; -/** - * Display field - * - * @var string - */ - public $displayField = 'field'; + /** + * Display field + * + * @var string + */ + public $displayField = 'field'; } diff --git a/lib/Cake/Model/Model.php b/lib/Cake/Model/Model.php index 860ad5bd..cd776ef7 100755 --- a/lib/Cake/Model/Model.php +++ b/lib/Cake/Model/Model.php @@ -42,3884 +42,3940 @@ * @package Cake.Model * @link https://book.cakephp.org/2.0/en/models.html */ -class Model extends CakeObject implements CakeEventListener { - -/** - * The name of the DataSource connection that this Model uses - * - * The value must be an attribute name that you defined in `app/Config/database.php` - * or created using `ConnectionManager::create()`. - * - * @var string - * @link https://book.cakephp.org/2.0/en/models/model-attributes.html#usedbconfig - */ - public $useDbConfig = 'default'; - -/** - * Custom database table name, or null/false if no table association is desired. - * - * @var string|false - * @link https://book.cakephp.org/2.0/en/models/model-attributes.html#usetable - */ - public $useTable = null; - -/** - * Custom display field name. Display fields are used by Scaffold, in SELECT boxes' OPTION elements. - * - * This field is also used in `find('list')` when called with no extra parameters in the fields list - * - * @var string|false - * @link https://book.cakephp.org/2.0/en/models/model-attributes.html#displayfield - */ - public $displayField = null; - -/** - * Value of the primary key ID of the record that this model is currently pointing to. - * Automatically set after database insertions. - * - * @var mixed - */ - public $id = false; - -/** - * Container for the data that this model gets from persistent storage (usually, a database). - * - * @var array|false - * @link https://book.cakephp.org/2.0/en/models/model-attributes.html#data - */ - public $data = array(); - -/** - * Holds physical schema/database name for this model. Automatically set during Model creation. - * - * @var string - */ - public $schemaName = null; - -/** - * Table name for this Model. - * - * @var string - */ - public $table = false; - -/** - * The name of the primary key field for this model. - * - * @var string - * @link https://book.cakephp.org/2.0/en/models/model-attributes.html#primarykey - */ - public $primaryKey = null; - -/** - * Field-by-field table metadata. - * - * @var array - */ - protected $_schema = null; - -/** - * List of validation rules. It must be an array with the field name as key and using - * as value one of the following possibilities - * - * ### Validating using regular expressions - * - * ``` - * public $validate = array( - * 'name' => '/^[a-z].+$/i' - * ); - * ``` - * - * ### Validating using methods (no parameters) - * - * ``` - * public $validate = array( - * 'name' => 'notBlank' - * ); - * ``` - * - * ### Validating using methods (with parameters) - * - * ``` - * public $validate = array( - * 'length' => array( - * 'rule' => array('lengthBetween', 5, 25) - * ) - * ); - * ``` - * - * ### Validating using custom method - * - * ``` - * public $validate = array( - * 'password' => array( - * 'rule' => array('customValidation') - * ) - * ); - * public function customValidation($data) { - * // $data will contain array('password' => 'value') - * if (isset($this->data[$this->alias]['password2'])) { - * return $this->data[$this->alias]['password2'] === current($data); - * } - * return true; - * } - * ``` - * - * ### Validations with messages - * - * The messages will be used in Model::$validationErrors and can be used in the FormHelper - * - * ``` - * public $validate = array( - * 'length' => array( - * 'rule' => array('lengthBetween', 5, 15), - * 'message' => array('Between %d to %d characters') - * ) - * ); - * ``` - * - * ### Multiple validations to the same field - * - * ``` - * public $validate = array( - * 'login' => array( - * array( - * 'rule' => 'alphaNumeric', - * 'message' => 'Only alphabets and numbers allowed', - * 'last' => true - * ), - * array( - * 'rule' => array('minLength', 8), - * 'message' => array('Minimum length of %d characters') - * ) - * ) - * ); - * ``` - * - * ### Valid keys in validations - * - * - `rule`: String with method name, regular expression (started by slash) or array with method and parameters - * - `message`: String with the message or array if have multiple parameters. See http://php.net/sprintf - * - `last`: Boolean value to indicate if continue validating the others rules if the current fail [Default: true] - * - `required`: Boolean value to indicate if the field must be present on save - * - `allowEmpty`: Boolean value to indicate if the field can be empty - * - `on`: Possible values: `update`, `create`. Indicate to apply this rule only on update or create - * - * @var array - * @link https://book.cakephp.org/2.0/en/models/model-attributes.html#validate - * @link https://book.cakephp.org/2.0/en/models/data-validation.html - */ - public $validate = array(); - -/** - * List of validation errors. - * - * @var array - */ - public $validationErrors = array(); - -/** - * Name of the validation string domain to use when translating validation errors. - * - * @var string - */ - public $validationDomain = null; - -/** - * Database table prefix for tables in model. - * - * @var string - * @link https://book.cakephp.org/2.0/en/models/model-attributes.html#tableprefix - */ - public $tablePrefix = null; - -/** - * Plugin model belongs to. - * - * @var string - */ - public $plugin = null; - -/** - * Name of the model. - * - * @var string - * @link https://book.cakephp.org/2.0/en/models/model-attributes.html#name - */ - public $name = null; - -/** - * Alias name for model. - * - * @var string - */ - public $alias = null; - -/** - * List of table names included in the model description. Used for associations. - * - * @var array - */ - public $tableToModel = array(); - -/** - * Whether or not to cache queries for this model. This enables in-memory - * caching only, the results are not stored beyond the current request. - * - * @var bool - * @link https://book.cakephp.org/2.0/en/models/model-attributes.html#cachequeries - */ - public $cacheQueries = false; - -/** - * Detailed list of belongsTo associations. - * - * ### Basic usage - * - * `public $belongsTo = array('Group', 'Department');` - * - * ### Detailed configuration - * - * ``` - * public $belongsTo = array( - * 'Group', - * 'Department' => array( - * 'className' => 'Department', - * 'foreignKey' => 'department_id' - * ) - * ); - * ``` - * - * ### Possible keys in association - * - * - `className`: the class name of the model being associated to the current model. - * If you're defining a 'Profile belongsTo User' relationship, the className key should equal 'User.' - * - `foreignKey`: the name of the foreign key found in the current model. This is - * especially handy if you need to define multiple belongsTo relationships. The default - * value for this key is the underscored, singular name of the other model, suffixed with '_id'. - * - `conditions`: An SQL fragment used to filter related model records. It's good - * practice to use model names in SQL fragments: 'User.active = 1' is always - * better than just 'active = 1.' - * - `type`: the type of the join to use in the SQL query, default is LEFT which - * may not fit your needs in all situations, INNER may be helpful when you want - * everything from your main and associated models or nothing at all!(effective - * when used with some conditions of course). (NB: type value is in lower case - i.e. left, inner) - * - `fields`: A list of fields to be retrieved when the associated model data is - * fetched. Returns all fields by default. - * - `order`: An SQL fragment that defines the sorting order for the returned associated rows. - * - `counterCache`: If set to true the associated Model will automatically increase or - * decrease the "[singular_model_name]_count" field in the foreign table whenever you do - * a save() or delete(). If its a string then its the field name to use. The value in the - * counter field represents the number of related rows. - * - `counterScope`: Optional conditions array to use for updating counter cache field. - * - * @var array - * @link https://book.cakephp.org/2.0/en/models/associations-linking-models-together.html#belongsto - */ - public $belongsTo = array(); - -/** - * Detailed list of hasOne associations. - * - * ### Basic usage - * - * `public $hasOne = array('Profile', 'Address');` - * - * ### Detailed configuration - * - * ``` - * public $hasOne = array( - * 'Profile', - * 'Address' => array( - * 'className' => 'Address', - * 'foreignKey' => 'user_id' - * ) - * ); - * ``` - * - * ### Possible keys in association - * - * - `className`: the class name of the model being associated to the current model. - * If you're defining a 'User hasOne Profile' relationship, the className key should equal 'Profile.' - * - `foreignKey`: the name of the foreign key found in the other model. This is - * especially handy if you need to define multiple hasOne relationships. - * The default value for this key is the underscored, singular name of the - * current model, suffixed with '_id'. In the example above it would default to 'user_id'. - * - `conditions`: An SQL fragment used to filter related model records. It's good - * practice to use model names in SQL fragments: "Profile.approved = 1" is - * always better than just "approved = 1." - * - `fields`: A list of fields to be retrieved when the associated model data is - * fetched. Returns all fields by default. - * - `order`: An SQL fragment that defines the sorting order for the returned associated rows. - * - `dependent`: When the dependent key is set to true, and the model's delete() - * method is called with the cascade parameter set to true, associated model - * records are also deleted. In this case we set it true so that deleting a - * User will also delete her associated Profile. - * - * @var array - * @link https://book.cakephp.org/2.0/en/models/associations-linking-models-together.html#hasone - */ - public $hasOne = array(); - -/** - * Detailed list of hasMany associations. - * - * ### Basic usage - * - * `public $hasMany = array('Comment', 'Task');` - * - * ### Detailed configuration - * - * ``` - * public $hasMany = array( - * 'Comment', - * 'Task' => array( - * 'className' => 'Task', - * 'foreignKey' => 'user_id' - * ) - * ); - * ``` - * - * ### Possible keys in association - * - * - `className`: the class name of the model being associated to the current model. - * If you're defining a 'User hasMany Comment' relationship, the className key should equal 'Comment.' - * - `foreignKey`: the name of the foreign key found in the other model. This is - * especially handy if you need to define multiple hasMany relationships. The default - * value for this key is the underscored, singular name of the actual model, suffixed with '_id'. - * - `conditions`: An SQL fragment used to filter related model records. It's good - * practice to use model names in SQL fragments: "Comment.status = 1" is always - * better than just "status = 1." - * - `fields`: A list of fields to be retrieved when the associated model data is - * fetched. Returns all fields by default. - * - `order`: An SQL fragment that defines the sorting order for the returned associated rows. - * - `limit`: The maximum number of associated rows you want returned. - * - `offset`: The number of associated rows to skip over (given the current - * conditions and order) before fetching and associating. - * - `dependent`: When dependent is set to true, recursive model deletion is - * possible. In this example, Comment records will be deleted when their - * associated User record has been deleted. - * - `exclusive`: When exclusive is set to true, recursive model deletion does - * the delete with a deleteAll() call, instead of deleting each entity separately. - * This greatly improves performance, but may not be ideal for all circumstances. - * - `finderQuery`: A complete SQL query CakePHP can use to fetch associated model - * records. This should be used in situations that require very custom results. - * - * @var array - * @link https://book.cakephp.org/2.0/en/models/associations-linking-models-together.html#hasmany - */ - public $hasMany = array(); - -/** - * Detailed list of hasAndBelongsToMany associations. - * - * ### Basic usage - * - * `public $hasAndBelongsToMany = array('Role', 'Address');` - * - * ### Detailed configuration - * - * ``` - * public $hasAndBelongsToMany = array( - * 'Role', - * 'Address' => array( - * 'className' => 'Address', - * 'foreignKey' => 'user_id', - * 'associationForeignKey' => 'address_id', - * 'joinTable' => 'addresses_users' - * ) - * ); - * ``` - * - * ### Possible keys in association - * - * - `className`: the class name of the model being associated to the current model. - * If you're defining a 'Recipe HABTM Tag' relationship, the className key should equal 'Tag.' - * - `joinTable`: The name of the join table used in this association (if the - * current table doesn't adhere to the naming convention for HABTM join tables). - * - `with`: Defines the name of the model for the join table. By default CakePHP - * will auto-create a model for you. Using the example above it would be called - * RecipesTag. By using this key you can override this default name. The join - * table model can be used just like any "regular" model to access the join table directly. - * - `foreignKey`: the name of the foreign key found in the current model. - * This is especially handy if you need to define multiple HABTM relationships. - * The default value for this key is the underscored, singular name of the - * current model, suffixed with '_id'. - * - `associationForeignKey`: the name of the foreign key found in the other model. - * This is especially handy if you need to define multiple HABTM relationships. - * The default value for this key is the underscored, singular name of the other - * model, suffixed with '_id'. - * - `unique`: If true (default value) cake will first delete existing relationship - * records in the foreign keys table before inserting new ones, when updating a - * record. So existing associations need to be passed again when updating. - * To prevent deletion of existing relationship records, set this key to a string 'keepExisting'. - * - `conditions`: An SQL fragment used to filter related model records. It's good - * practice to use model names in SQL fragments: "Comment.status = 1" is always - * better than just "status = 1." - * - `fields`: A list of fields to be retrieved when the associated model data is - * fetched. Returns all fields by default. - * - `order`: An SQL fragment that defines the sorting order for the returned associated rows. - * - `limit`: The maximum number of associated rows you want returned. - * - `offset`: The number of associated rows to skip over (given the current - * conditions and order) before fetching and associating. - * - `finderQuery`, A complete SQL query CakePHP - * can use to fetch associated model records. This should - * be used in situations that require very custom results. - * - * @var array - * @link https://book.cakephp.org/2.0/en/models/associations-linking-models-together.html#hasandbelongstomany-habtm - */ - public $hasAndBelongsToMany = array(); - -/** - * List of behaviors to load when the model object is initialized. Settings can be - * passed to behaviors by using the behavior name as index. - * - * For example: - * - * ``` - * public $actsAs = array( - * 'Translate', - * 'MyBehavior' => array('setting1' => 'value1') - * ); - * ``` - * - * @var array - * @link https://book.cakephp.org/2.0/en/models/behaviors.html#using-behaviors - */ - public $actsAs = null; - -/** - * Holds the Behavior objects currently bound to this model. - * - * @var BehaviorCollection - */ - public $Behaviors = null; - -/** - * Whitelist of fields allowed to be saved. - * - * @var array - */ - public $whitelist = array(); - -/** - * Whether or not to cache sources for this model. - * - * @var bool - */ - public $cacheSources = true; - -/** - * Type of find query currently executing. - * - * @var string - */ - public $findQueryType = null; - -/** - * Number of associations to recurse through during find calls. Fetches only - * the first level by default. - * - * @var int - * @link https://book.cakephp.org/2.0/en/models/model-attributes.html#recursive - */ - public $recursive = 1; - -/** - * The column name(s) and direction(s) to order find results by default. - * - * public $order = "Post.created DESC"; - * public $order = array("Post.view_count DESC", "Post.rating DESC"); - * - * @var string - * @link https://book.cakephp.org/2.0/en/models/model-attributes.html#order - */ - public $order = null; - -/** - * Array of virtual fields this model has. Virtual fields are aliased - * SQL expressions. Fields added to this property will be read as other fields in a model - * but will not be saveable. - * - * `public $virtualFields = array('two' => '1 + 1');` - * - * Is a simplistic example of how to set virtualFields - * - * @var array - * @link https://book.cakephp.org/2.0/en/models/model-attributes.html#virtualfields - */ - public $virtualFields = array(); - -/** - * Default list of association keys. - * - * @var array - */ - protected $_associationKeys = array( - 'belongsTo' => array('className', 'foreignKey', 'conditions', 'fields', 'order', 'counterCache'), - 'hasOne' => array('className', 'foreignKey', 'conditions', 'fields', 'order', 'dependent'), - 'hasMany' => array('className', 'foreignKey', 'conditions', 'fields', 'order', 'limit', 'offset', 'dependent', 'exclusive', 'finderQuery', 'counterQuery'), - 'hasAndBelongsToMany' => array('className', 'joinTable', 'with', 'foreignKey', 'associationForeignKey', 'conditions', 'fields', 'order', 'limit', 'offset', 'unique', 'finderQuery') - ); - -/** - * Holds provided/generated association key names and other data for all associations. - * - * @var array - */ - protected $_associations = array('belongsTo', 'hasOne', 'hasMany', 'hasAndBelongsToMany'); +class Model extends CakeObject implements CakeEventListener +{ + + /** + * The name of the DataSource connection that this Model uses + * + * The value must be an attribute name that you defined in `app/Config/database.php` + * or created using `ConnectionManager::create()`. + * + * @var string + * @link https://book.cakephp.org/2.0/en/models/model-attributes.html#usedbconfig + */ + public $useDbConfig = 'default'; + + /** + * Custom database table name, or null/false if no table association is desired. + * + * @var string|false + * @link https://book.cakephp.org/2.0/en/models/model-attributes.html#usetable + */ + public $useTable = null; + + /** + * Custom display field name. Display fields are used by Scaffold, in SELECT boxes' OPTION elements. + * + * This field is also used in `find('list')` when called with no extra parameters in the fields list + * + * @var string|false + * @link https://book.cakephp.org/2.0/en/models/model-attributes.html#displayfield + */ + public $displayField = null; + + /** + * Value of the primary key ID of the record that this model is currently pointing to. + * Automatically set after database insertions. + * + * @var mixed + */ + public $id = false; + + /** + * Container for the data that this model gets from persistent storage (usually, a database). + * + * @var array|false + * @link https://book.cakephp.org/2.0/en/models/model-attributes.html#data + */ + public $data = []; + + /** + * Holds physical schema/database name for this model. Automatically set during Model creation. + * + * @var string + */ + public $schemaName = null; + + /** + * Table name for this Model. + * + * @var string + */ + public $table = false; + + /** + * The name of the primary key field for this model. + * + * @var string + * @link https://book.cakephp.org/2.0/en/models/model-attributes.html#primarykey + */ + public $primaryKey = null; + /** + * List of validation rules. It must be an array with the field name as key and using + * as value one of the following possibilities + * + * ### Validating using regular expressions + * + * ``` + * public $validate = array( + * 'name' => '/^[a-z].+$/i' + * ); + * ``` + * + * ### Validating using methods (no parameters) + * + * ``` + * public $validate = array( + * 'name' => 'notBlank' + * ); + * ``` + * + * ### Validating using methods (with parameters) + * + * ``` + * public $validate = array( + * 'length' => array( + * 'rule' => array('lengthBetween', 5, 25) + * ) + * ); + * ``` + * + * ### Validating using custom method + * + * ``` + * public $validate = array( + * 'password' => array( + * 'rule' => array('customValidation') + * ) + * ); + * public function customValidation($data) { + * // $data will contain array('password' => 'value') + * if (isset($this->data[$this->alias]['password2'])) { + * return $this->data[$this->alias]['password2'] === current($data); + * } + * return true; + * } + * ``` + * + * ### Validations with messages + * + * The messages will be used in Model::$validationErrors and can be used in the FormHelper + * + * ``` + * public $validate = array( + * 'length' => array( + * 'rule' => array('lengthBetween', 5, 15), + * 'message' => array('Between %d to %d characters') + * ) + * ); + * ``` + * + * ### Multiple validations to the same field + * + * ``` + * public $validate = array( + * 'login' => array( + * array( + * 'rule' => 'alphaNumeric', + * 'message' => 'Only alphabets and numbers allowed', + * 'last' => true + * ), + * array( + * 'rule' => array('minLength', 8), + * 'message' => array('Minimum length of %d characters') + * ) + * ) + * ); + * ``` + * + * ### Valid keys in validations + * + * - `rule`: String with method name, regular expression (started by slash) or array with method and parameters + * - `message`: String with the message or array if have multiple parameters. See http://php.net/sprintf + * - `last`: Boolean value to indicate if continue validating the others rules if the current fail [Default: true] + * - `required`: Boolean value to indicate if the field must be present on save + * - `allowEmpty`: Boolean value to indicate if the field can be empty + * - `on`: Possible values: `update`, `create`. Indicate to apply this rule only on update or create + * + * @var array + * @link https://book.cakephp.org/2.0/en/models/model-attributes.html#validate + * @link https://book.cakephp.org/2.0/en/models/data-validation.html + */ + public $validate = []; + /** + * List of validation errors. + * + * @var array + */ + public $validationErrors = []; + /** + * Name of the validation string domain to use when translating validation errors. + * + * @var string + */ + public $validationDomain = null; + /** + * Database table prefix for tables in model. + * + * @var string + * @link https://book.cakephp.org/2.0/en/models/model-attributes.html#tableprefix + */ + public $tablePrefix = null; + /** + * Plugin model belongs to. + * + * @var string + */ + public $plugin = null; + /** + * Name of the model. + * + * @var string + * @link https://book.cakephp.org/2.0/en/models/model-attributes.html#name + */ + public $name = null; + /** + * Alias name for model. + * + * @var string + */ + public $alias = null; + /** + * List of table names included in the model description. Used for associations. + * + * @var array + */ + public $tableToModel = []; + /** + * Whether or not to cache queries for this model. This enables in-memory + * caching only, the results are not stored beyond the current request. + * + * @var bool + * @link https://book.cakephp.org/2.0/en/models/model-attributes.html#cachequeries + */ + public $cacheQueries = false; + /** + * Detailed list of belongsTo associations. + * + * ### Basic usage + * + * `public $belongsTo = array('Group', 'Department');` + * + * ### Detailed configuration + * + * ``` + * public $belongsTo = array( + * 'Group', + * 'Department' => array( + * 'className' => 'Department', + * 'foreignKey' => 'department_id' + * ) + * ); + * ``` + * + * ### Possible keys in association + * + * - `className`: the class name of the model being associated to the current model. + * If you're defining a 'Profile belongsTo User' relationship, the className key should equal 'User.' + * - `foreignKey`: the name of the foreign key found in the current model. This is + * especially handy if you need to define multiple belongsTo relationships. The default + * value for this key is the underscored, singular name of the other model, suffixed with '_id'. + * - `conditions`: An SQL fragment used to filter related model records. It's good + * practice to use model names in SQL fragments: 'User.active = 1' is always + * better than just 'active = 1.' + * - `type`: the type of the join to use in the SQL query, default is LEFT which + * may not fit your needs in all situations, INNER may be helpful when you want + * everything from your main and associated models or nothing at all!(effective + * when used with some conditions of course). (NB: type value is in lower case - i.e. left, inner) + * - `fields`: A list of fields to be retrieved when the associated model data is + * fetched. Returns all fields by default. + * - `order`: An SQL fragment that defines the sorting order for the returned associated rows. + * - `counterCache`: If set to true the associated Model will automatically increase or + * decrease the "[singular_model_name]_count" field in the foreign table whenever you do + * a save() or delete(). If its a string then its the field name to use. The value in the + * counter field represents the number of related rows. + * - `counterScope`: Optional conditions array to use for updating counter cache field. + * + * @var array + * @link https://book.cakephp.org/2.0/en/models/associations-linking-models-together.html#belongsto + */ + public $belongsTo = []; + /** + * Detailed list of hasOne associations. + * + * ### Basic usage + * + * `public $hasOne = array('Profile', 'Address');` + * + * ### Detailed configuration + * + * ``` + * public $hasOne = array( + * 'Profile', + * 'Address' => array( + * 'className' => 'Address', + * 'foreignKey' => 'user_id' + * ) + * ); + * ``` + * + * ### Possible keys in association + * + * - `className`: the class name of the model being associated to the current model. + * If you're defining a 'User hasOne Profile' relationship, the className key should equal 'Profile.' + * - `foreignKey`: the name of the foreign key found in the other model. This is + * especially handy if you need to define multiple hasOne relationships. + * The default value for this key is the underscored, singular name of the + * current model, suffixed with '_id'. In the example above it would default to 'user_id'. + * - `conditions`: An SQL fragment used to filter related model records. It's good + * practice to use model names in SQL fragments: "Profile.approved = 1" is + * always better than just "approved = 1." + * - `fields`: A list of fields to be retrieved when the associated model data is + * fetched. Returns all fields by default. + * - `order`: An SQL fragment that defines the sorting order for the returned associated rows. + * - `dependent`: When the dependent key is set to true, and the model's delete() + * method is called with the cascade parameter set to true, associated model + * records are also deleted. In this case we set it true so that deleting a + * User will also delete her associated Profile. + * + * @var array + * @link https://book.cakephp.org/2.0/en/models/associations-linking-models-together.html#hasone + */ + public $hasOne = []; + /** + * Detailed list of hasMany associations. + * + * ### Basic usage + * + * `public $hasMany = array('Comment', 'Task');` + * + * ### Detailed configuration + * + * ``` + * public $hasMany = array( + * 'Comment', + * 'Task' => array( + * 'className' => 'Task', + * 'foreignKey' => 'user_id' + * ) + * ); + * ``` + * + * ### Possible keys in association + * + * - `className`: the class name of the model being associated to the current model. + * If you're defining a 'User hasMany Comment' relationship, the className key should equal 'Comment.' + * - `foreignKey`: the name of the foreign key found in the other model. This is + * especially handy if you need to define multiple hasMany relationships. The default + * value for this key is the underscored, singular name of the actual model, suffixed with '_id'. + * - `conditions`: An SQL fragment used to filter related model records. It's good + * practice to use model names in SQL fragments: "Comment.status = 1" is always + * better than just "status = 1." + * - `fields`: A list of fields to be retrieved when the associated model data is + * fetched. Returns all fields by default. + * - `order`: An SQL fragment that defines the sorting order for the returned associated rows. + * - `limit`: The maximum number of associated rows you want returned. + * - `offset`: The number of associated rows to skip over (given the current + * conditions and order) before fetching and associating. + * - `dependent`: When dependent is set to true, recursive model deletion is + * possible. In this example, Comment records will be deleted when their + * associated User record has been deleted. + * - `exclusive`: When exclusive is set to true, recursive model deletion does + * the delete with a deleteAll() call, instead of deleting each entity separately. + * This greatly improves performance, but may not be ideal for all circumstances. + * - `finderQuery`: A complete SQL query CakePHP can use to fetch associated model + * records. This should be used in situations that require very custom results. + * + * @var array + * @link https://book.cakephp.org/2.0/en/models/associations-linking-models-together.html#hasmany + */ + public $hasMany = []; + /** + * Detailed list of hasAndBelongsToMany associations. + * + * ### Basic usage + * + * `public $hasAndBelongsToMany = array('Role', 'Address');` + * + * ### Detailed configuration + * + * ``` + * public $hasAndBelongsToMany = array( + * 'Role', + * 'Address' => array( + * 'className' => 'Address', + * 'foreignKey' => 'user_id', + * 'associationForeignKey' => 'address_id', + * 'joinTable' => 'addresses_users' + * ) + * ); + * ``` + * + * ### Possible keys in association + * + * - `className`: the class name of the model being associated to the current model. + * If you're defining a 'Recipe HABTM Tag' relationship, the className key should equal 'Tag.' + * - `joinTable`: The name of the join table used in this association (if the + * current table doesn't adhere to the naming convention for HABTM join tables). + * - `with`: Defines the name of the model for the join table. By default CakePHP + * will auto-create a model for you. Using the example above it would be called + * RecipesTag. By using this key you can override this default name. The join + * table model can be used just like any "regular" model to access the join table directly. + * - `foreignKey`: the name of the foreign key found in the current model. + * This is especially handy if you need to define multiple HABTM relationships. + * The default value for this key is the underscored, singular name of the + * current model, suffixed with '_id'. + * - `associationForeignKey`: the name of the foreign key found in the other model. + * This is especially handy if you need to define multiple HABTM relationships. + * The default value for this key is the underscored, singular name of the other + * model, suffixed with '_id'. + * - `unique`: If true (default value) cake will first delete existing relationship + * records in the foreign keys table before inserting new ones, when updating a + * record. So existing associations need to be passed again when updating. + * To prevent deletion of existing relationship records, set this key to a string 'keepExisting'. + * - `conditions`: An SQL fragment used to filter related model records. It's good + * practice to use model names in SQL fragments: "Comment.status = 1" is always + * better than just "status = 1." + * - `fields`: A list of fields to be retrieved when the associated model data is + * fetched. Returns all fields by default. + * - `order`: An SQL fragment that defines the sorting order for the returned associated rows. + * - `limit`: The maximum number of associated rows you want returned. + * - `offset`: The number of associated rows to skip over (given the current + * conditions and order) before fetching and associating. + * - `finderQuery`, A complete SQL query CakePHP + * can use to fetch associated model records. This should + * be used in situations that require very custom results. + * + * @var array + * @link https://book.cakephp.org/2.0/en/models/associations-linking-models-together.html#hasandbelongstomany-habtm + */ + public $hasAndBelongsToMany = []; + /** + * List of behaviors to load when the model object is initialized. Settings can be + * passed to behaviors by using the behavior name as index. + * + * For example: + * + * ``` + * public $actsAs = array( + * 'Translate', + * 'MyBehavior' => array('setting1' => 'value1') + * ); + * ``` + * + * @var array + * @link https://book.cakephp.org/2.0/en/models/behaviors.html#using-behaviors + */ + public $actsAs = null; + /** + * Holds the Behavior objects currently bound to this model. + * + * @var BehaviorCollection + */ + public $Behaviors = null; + /** + * Whitelist of fields allowed to be saved. + * + * @var array + */ + public $whitelist = []; + /** + * Whether or not to cache sources for this model. + * + * @var bool + */ + public $cacheSources = true; + /** + * Type of find query currently executing. + * + * @var string + */ + public $findQueryType = null; + /** + * Number of associations to recurse through during find calls. Fetches only + * the first level by default. + * + * @var int + * @link https://book.cakephp.org/2.0/en/models/model-attributes.html#recursive + */ + public $recursive = 1; + /** + * The column name(s) and direction(s) to order find results by default. + * + * public $order = "Post.created DESC"; + * public $order = array("Post.view_count DESC", "Post.rating DESC"); + * + * @var string + * @link https://book.cakephp.org/2.0/en/models/model-attributes.html#order + */ + public $order = null; + /** + * Array of virtual fields this model has. Virtual fields are aliased + * SQL expressions. Fields added to this property will be read as other fields in a model + * but will not be saveable. + * + * `public $virtualFields = array('two' => '1 + 1');` + * + * Is a simplistic example of how to set virtualFields + * + * @var array + * @link https://book.cakephp.org/2.0/en/models/model-attributes.html#virtualfields + */ + public $virtualFields = []; + /** + * Holds model associations temporarily to allow for dynamic (un)binding. + * + * @var array + */ + public $__backAssociation = []; + /** + * Back inner association + * + * @var array + */ + public $__backInnerAssociation = []; + /** + * Back original association + * + * @var array + */ + public $__backOriginalAssociation = []; // @codingStandardsIgnoreStart - -/** - * Holds model associations temporarily to allow for dynamic (un)binding. - * - * @var array - */ - public $__backAssociation = array(); - -/** - * Back inner association - * - * @var array - */ - public $__backInnerAssociation = array(); - -/** - * Back original association - * - * @var array - */ - public $__backOriginalAssociation = array(); - -/** - * Back containable association - * - * @var array - */ - public $__backContainableAssociation = array(); - -/** - * Safe update mode - * If true, this prevents Model::save() from generating a query with WHERE 1 = 1 on race condition. - * - * @var bool - */ - public $__safeUpdateMode = false; + /** + * Back containable association + * + * @var array + */ + public $__backContainableAssociation = []; + /** + * Safe update mode + * If true, this prevents Model::save() from generating a query with WHERE 1 = 1 on race condition. + * + * @var bool + */ + public $__safeUpdateMode = false; + /** + * If true, afterFind will be passed consistent formatted $results in case of $primary is false. + * The format will be such as the following. + * + * ``` + * $results = array( + * 0 => array( + * 'ModelName' => array( + * 'field1' => 'value1', + * 'field2' => 'value2' + * ) + * ) + * ); + * ``` + * + * @var bool + */ + public $useConsistentAfterFind = true; + /** + * List of valid finder method options, supplied as the first parameter to find(). + * + * @var array + */ + public $findMethods = [ + 'all' => true, 'first' => true, 'count' => true, + 'neighbors' => true, 'list' => true, 'threaded' => true + ]; + /** + * Field-by-field table metadata. + * + * @var array + */ + protected $_schema = null; // @codingStandardsIgnoreEnd - -/** - * If true, afterFind will be passed consistent formatted $results in case of $primary is false. - * The format will be such as the following. - * - * ``` - * $results = array( - * 0 => array( - * 'ModelName' => array( - * 'field1' => 'value1', - * 'field2' => 'value2' - * ) - * ) - * ); - * ``` - * - * @var bool - */ - public $useConsistentAfterFind = true; - -/** - * The ID of the model record that was last inserted. - * - * @var int|string - */ - protected $_insertID = null; - -/** - * Has the datasource been configured. - * - * @var bool - * @see Model::getDataSource - */ - protected $_sourceConfigured = false; - -/** - * List of valid finder method options, supplied as the first parameter to find(). - * - * @var array - */ - public $findMethods = array( - 'all' => true, 'first' => true, 'count' => true, - 'neighbors' => true, 'list' => true, 'threaded' => true - ); - -/** - * Instance of the CakeEventManager this model is using - * to dispatch inner events. - * - * @var CakeEventManager - */ - protected $_eventManager = null; - -/** - * Instance of the ModelValidator - * - * @var ModelValidator - */ - protected $_validator = null; - -/** - * Constructor. Binds the model's database table to the object. - * - * If `$id` is an array it can be used to pass several options into the model. - * - * - `id`: The id to start the model on. - * - `table`: The table to use for this model. - * - `ds`: The connection name this model is connected to. - * - `name`: The name of the model eg. Post. - * - `alias`: The alias of the model, this is used for registering the instance in the `ClassRegistry`. - * eg. `ParentThread` - * - * ### Overriding Model's __construct method. - * - * When overriding Model::__construct() be careful to include and pass in all 3 of the - * arguments to `parent::__construct($id, $table, $ds);` - * - * ### Dynamically creating models - * - * You can dynamically create model instances using the $id array syntax. - * - * ``` - * $Post = new Model(array('table' => 'posts', 'name' => 'Post', 'ds' => 'connection2')); - * ``` - * - * Would create a model attached to the posts table on connection2. Dynamic model creation is useful - * when you want a model object that contains no associations or attached behaviors. - * - * @param bool|int|string|array $id Set this ID for this model on startup, - * can also be an array of options, see above. - * @param string|false $table Name of database table to use. - * @param string $ds DataSource connection name. - */ - public function __construct($id = false, $table = null, $ds = null) { - parent::__construct(); - - if (is_array($id)) { - extract(array_merge( - array( - 'id' => $this->id, 'table' => $this->useTable, 'ds' => $this->useDbConfig, - 'name' => $this->name, 'alias' => $this->alias, 'plugin' => $this->plugin - ), - $id - )); - } - - if ($this->plugin === null) { - $this->plugin = (isset($plugin) ? $plugin : $this->plugin); - } - - if ($this->name === null) { - $this->name = (isset($name) ? $name : get_class($this)); - } - - if ($this->alias === null) { - $this->alias = (isset($alias) ? $alias : $this->name); - } - - if ($this->primaryKey === null) { - $this->primaryKey = 'id'; - } - - ClassRegistry::addObject($this->alias, $this); - - $this->id = $id; - unset($id); - - if ($table === false) { - $this->useTable = false; - } elseif ($table) { - $this->useTable = $table; - } - - if ($ds !== null) { - $this->useDbConfig = $ds; - } - - if (is_subclass_of($this, 'AppModel')) { - $merge = array('actsAs', 'findMethods'); - $parentClass = get_parent_class($this); - if ($parentClass !== 'AppModel') { - $this->_mergeVars($merge, $parentClass); - } - $this->_mergeVars($merge, 'AppModel'); - } - $this->_mergeVars(array('findMethods'), 'Model'); - - $this->Behaviors = new BehaviorCollection(); - - if ($this->useTable !== false) { - - if ($this->useTable === null) { - $this->useTable = Inflector::tableize($this->name); - } - - if (!$this->displayField) { - unset($this->displayField); - } - $this->table = $this->useTable; - $this->tableToModel[$this->table] = $this->alias; - } elseif ($this->table === false) { - $this->table = Inflector::tableize($this->name); - } - - if ($this->tablePrefix === null) { - unset($this->tablePrefix); - } - - $this->_createLinks(); - $this->Behaviors->init($this->alias, $this->actsAs); - } - -/** - * Returns a list of all events that will fire in the model during it's lifecycle. - * You can override this function to add your own listener callbacks - * - * @return array - */ - public function implementedEvents() { - return array( - 'Model.beforeFind' => array('callable' => 'beforeFind', 'passParams' => true), - 'Model.afterFind' => array('callable' => 'afterFind', 'passParams' => true), - 'Model.beforeValidate' => array('callable' => 'beforeValidate', 'passParams' => true), - 'Model.afterValidate' => array('callable' => 'afterValidate'), - 'Model.beforeSave' => array('callable' => 'beforeSave', 'passParams' => true), - 'Model.afterSave' => array('callable' => 'afterSave', 'passParams' => true), - 'Model.beforeDelete' => array('callable' => 'beforeDelete', 'passParams' => true), - 'Model.afterDelete' => array('callable' => 'afterDelete'), - ); - } - -/** - * Returns the CakeEventManager manager instance that is handling any callbacks. - * You can use this instance to register any new listeners or callbacks to the - * model events, or create your own events and trigger them at will. - * - * @return CakeEventManager - */ - public function getEventManager() { - if (empty($this->_eventManager)) { - $this->_eventManager = new CakeEventManager(); - $this->_eventManager->attach($this->Behaviors); - $this->_eventManager->attach($this); - } - - return $this->_eventManager; - } - -/** - * Handles custom method calls, like findBy for DB models, - * and custom RPC calls for remote data sources. - * - * @param string $method Name of method to call. - * @param array $params Parameters for the method. - * @return mixed Whatever is returned by called method - */ - public function __call($method, $params) { - $result = $this->Behaviors->dispatchMethod($this, $method, $params); - if ($result !== array('unhandled')) { - return $result; - } - - return $this->getDataSource()->query($method, $params, $this); - } - -/** - * Handles the lazy loading of model associations by looking in the association arrays for the requested variable - * - * @param string $name variable tested for existence in class - * @return bool true if the variable exists (if is a not loaded model association it will be created), false otherwise - */ - public function __isset($name) { - $className = false; - - foreach ($this->_associations as $type) { - if (isset($name, $this->{$type}[$name])) { - $className = empty($this->{$type}[$name]['className']) ? $name : $this->{$type}[$name]['className']; - break; - } elseif (isset($name, $this->__backAssociation[$type][$name])) { - $className = empty($this->__backAssociation[$type][$name]['className']) ? - $name : $this->__backAssociation[$type][$name]['className']; - break; - } elseif ($type === 'hasAndBelongsToMany') { - foreach ($this->{$type} as $k => $relation) { - if (empty($relation['with'])) { - continue; - } - - if (is_array($relation['with'])) { - if (key($relation['with']) === $name) { - $className = $name; - } - } else { - list($plugin, $class) = pluginSplit($relation['with']); - if ($class === $name) { - $className = $relation['with']; - } - } - - if ($className) { - $assocKey = $k; - $dynamic = !empty($relation['dynamicWith']); - break(2); - } - } - } - } - - if (!$className) { - return false; - } - - list($plugin, $className) = pluginSplit($className); - - if (!ClassRegistry::isKeySet($className) && !empty($dynamic)) { - $this->{$className} = new AppModel(array( - 'name' => $className, - 'table' => $this->hasAndBelongsToMany[$assocKey]['joinTable'], - 'ds' => $this->useDbConfig - )); - } else { - $this->_constructLinkedModel($name, $className, $plugin); - } - - if (!empty($assocKey)) { - $this->hasAndBelongsToMany[$assocKey]['joinTable'] = $this->{$name}->table; - if (count($this->{$name}->schema()) <= 2 && $this->{$name}->primaryKey !== false) { - $this->{$name}->primaryKey = $this->hasAndBelongsToMany[$assocKey]['foreignKey']; - } - } - - return true; - } - -/** - * Returns the value of the requested variable if it can be set by __isset() - * - * @param string $name variable requested for it's value or reference - * @return mixed value of requested variable if it is set - */ - public function __get($name) { - if ($name === 'displayField') { - return $this->displayField = $this->hasField(array('title', 'name', $this->primaryKey)); - } - - if ($name === 'tablePrefix') { - $this->setDataSource(); - if (property_exists($this, 'tablePrefix') && !empty($this->tablePrefix)) { - return $this->tablePrefix; - } - - return $this->tablePrefix = null; - } - - if (isset($this->{$name})) { - return $this->{$name}; - } - } - -/** - * Bind model associations on the fly. - * - * If `$reset` is false, association will not be reset - * to the originals defined in the model - * - * Example: Add a new hasOne binding to the Profile model not - * defined in the model source code: - * - * `$this->User->bindModel(array('hasOne' => array('Profile')));` - * - * Bindings that are not made permanent will be reset by the next Model::find() call on this - * model. - * - * @param array $params Set of bindings (indexed by binding type) - * @param bool $reset Set to false to make the binding permanent - * @return bool Success - * @link https://book.cakephp.org/2.0/en/models/associations-linking-models-together.html#creating-and-destroying-associations-on-the-fly - */ - public function bindModel($params, $reset = true) { - foreach ($params as $assoc => $model) { - if ($reset === true && !isset($this->__backAssociation[$assoc])) { - $this->__backAssociation[$assoc] = $this->{$assoc}; - } - - foreach ($model as $key => $value) { - $assocName = $key; - - if (is_numeric($key)) { - $assocName = $value; - $value = array(); - } - - $this->{$assoc}[$assocName] = $value; - - if (property_exists($this, $assocName)) { - unset($this->{$assocName}); - } - - if ($reset === false && isset($this->__backAssociation[$assoc])) { - $this->__backAssociation[$assoc][$assocName] = $value; - } - } - } - - $this->_createLinks(); - return true; - } - -/** - * Turn off associations on the fly. - * - * If $reset is false, association will not be reset - * to the originals defined in the model - * - * Example: Turn off the associated Model Support request, - * to temporarily lighten the User model: - * - * `$this->User->unbindModel(array('hasMany' => array('SupportRequest')));` - * Or alternatively: - * `$this->User->unbindModel(array('hasMany' => 'SupportRequest'));` - * - * Unbound models that are not made permanent will reset with the next call to Model::find() - * - * @param array $params Set of bindings to unbind (indexed by binding type) - * @param bool $reset Set to false to make the unbinding permanent - * @return bool Success - * @link https://book.cakephp.org/2.0/en/models/associations-linking-models-together.html#creating-and-destroying-associations-on-the-fly - */ - public function unbindModel($params, $reset = true) { - foreach ($params as $assoc => $models) { - if ($reset === true && !isset($this->__backAssociation[$assoc])) { - $this->__backAssociation[$assoc] = $this->{$assoc}; - } - $models = Hash::normalize((array)$models, false); - foreach ($models as $model) { - if ($reset === false && isset($this->__backAssociation[$assoc][$model])) { - unset($this->__backAssociation[$assoc][$model]); - } - - unset($this->{$assoc}[$model]); - } - } - - return true; - } - -/** - * Create a set of associations. - * - * @return void - */ - protected function _createLinks() { - foreach ($this->_associations as $type) { - $association =& $this->{$type}; - - if (!is_array($association)) { - $association = explode(',', $association); - - foreach ($association as $i => $className) { - $className = trim($className); - unset ($association[$i]); - $association[$className] = array(); - } - } - - if (!empty($association)) { - foreach ($association as $assoc => $value) { - $plugin = null; - - if (is_numeric($assoc)) { - unset($association[$assoc]); - $assoc = $value; - $value = array(); - $association[$assoc] = $value; - } - - if (!isset($value['className']) && strpos($assoc, '.') !== false) { - unset($association[$assoc]); - list($plugin, $assoc) = pluginSplit($assoc, true); - $association[$assoc] = array('className' => $plugin . $assoc) + $value; - } - - $this->_generateAssociation($type, $assoc); - } - } - } - } - -/** - * Protected helper method to create associated models of a given class. - * - * @param string $assoc Association name - * @param string $className Class name - * @param string $plugin name of the plugin where $className is located - * examples: public $hasMany = array('Assoc' => array('className' => 'ModelName')); - * usage: $this->Assoc->modelMethods(); - * - * public $hasMany = array('ModelName'); - * usage: $this->ModelName->modelMethods(); - * @return void - */ - protected function _constructLinkedModel($assoc, $className = null, $plugin = null) { - if (empty($className)) { - $className = $assoc; - } - - if (!isset($this->{$assoc}) || $this->{$assoc}->name !== $className) { - if ($plugin) { - $plugin .= '.'; - } - - $model = array('class' => $plugin . $className, 'alias' => $assoc); - $this->{$assoc} = ClassRegistry::init($model); - - if ($plugin) { - ClassRegistry::addObject($plugin . $className, $this->{$assoc}); - } - - if ($assoc) { - $this->tableToModel[$this->{$assoc}->table] = $assoc; - } - } - } - -/** - * Build an array-based association from string. - * - * @param string $type 'belongsTo', 'hasOne', 'hasMany', 'hasAndBelongsToMany' - * @param string $assocKey Association key. - * @return void - */ - protected function _generateAssociation($type, $assocKey) { - $class = $assocKey; - $dynamicWith = false; - $assoc =& $this->{$type}[$assocKey]; - - foreach ($this->_associationKeys[$type] as $key) { - if (!isset($assoc[$key]) || $assoc[$key] === null) { - $data = ''; - - switch ($key) { - case 'fields': - $data = ''; - break; - - case 'foreignKey': - $data = (($type === 'belongsTo') ? Inflector::underscore($assocKey) : Inflector::singularize($this->table)) . '_id'; - break; - - case 'associationForeignKey': - $data = Inflector::singularize($this->{$class}->table) . '_id'; - break; - - case 'with': - $data = Inflector::camelize(Inflector::singularize($assoc['joinTable'])); - $dynamicWith = true; - break; - - case 'joinTable': - $tables = array($this->table, $this->{$class}->table); - sort($tables); - $data = $tables[0] . '_' . $tables[1]; - break; - - case 'className': - $data = $class; - break; - - case 'unique': - $data = true; - break; - } - - $assoc[$key] = $data; - } - - if ($dynamicWith) { - $assoc['dynamicWith'] = true; - } - } - } - -/** - * Sets a custom table for your model class. Used by your controller to select a database table. - * - * @param string $tableName Name of the custom table - * @throws MissingTableException when database table $tableName is not found on data source - * @return void - */ - public function setSource($tableName) { - $this->setDataSource($this->useDbConfig); - $db = ConnectionManager::getDataSource($this->useDbConfig); - - if (method_exists($db, 'listSources')) { - $restore = $db->cacheSources; - $db->cacheSources = ($restore && $this->cacheSources); - $sources = $db->listSources(); - $db->cacheSources = $restore; - - if (is_array($sources) && !in_array(strtolower($this->tablePrefix . $tableName), array_map('strtolower', $sources))) { - throw new MissingTableException(array( - 'table' => $this->tablePrefix . $tableName, - 'class' => $this->alias, - 'ds' => $this->useDbConfig, - )); - } - - if ($sources) { - $this->_schema = null; - } - } - - $this->table = $this->useTable = $tableName; - $this->tableToModel[$this->table] = $this->alias; - } - -/** - * This function does two things: - * - * 1. it scans the array $one for the primary key, - * and if that's found, it sets the current id to the value of $one[id]. - * For all other keys than 'id' the keys and values of $one are copied to the 'data' property of this object. - * 2. Returns an array with all of $one's keys and values. - * (Alternative indata: two strings, which are mangled to - * a one-item, two-dimensional array using $one for a key and $two as its value.) - * - * @param string|array|SimpleXmlElement|DomNode $one Array or string of data - * @param string|false $two Value string for the alternative indata method - * @return array|null Data with all of $one's keys and values, otherwise null. - * @link https://book.cakephp.org/2.0/en/models/saving-your-data.html - */ - public function set($one, $two = null) { - if (!$one) { - return null; - } - - if (is_object($one)) { - if ($one instanceof SimpleXMLElement || $one instanceof DOMNode) { - $one = $this->_normalizeXmlData(Xml::toArray($one)); - } else { - $one = Set::reverse($one); - } - } - - if (is_array($one)) { - $data = $one; - if (empty($one[$this->alias])) { - $data = $this->_setAliasData($one); - } - } else { - $data = array($this->alias => array($one => $two)); - } - - foreach ($data as $modelName => $fieldSet) { - if (!is_array($fieldSet)) { - continue; - } - - if (!isset($this->data[$modelName])) { - $this->data[$modelName] = array(); - } - - foreach ($fieldSet as $fieldName => $fieldValue) { - unset($this->validationErrors[$fieldName]); - - if ($modelName === $this->alias && $fieldName === $this->primaryKey) { - $this->id = $fieldValue; - } - - if (is_array($fieldValue) || is_object($fieldValue)) { - $fieldValue = $this->deconstruct($fieldName, $fieldValue); - } - - $this->data[$modelName][$fieldName] = $fieldValue; - } - } - - return $data; - } - -/** - * Move values to alias - * - * @param array $data Data. - * @return array - */ - protected function _setAliasData($data) { - $models = array_keys($this->getAssociated()); - $schema = array_keys((array)$this->schema()); - - foreach ($data as $field => $value) { - if (in_array($field, $schema) || !in_array($field, $models)) { - $data[$this->alias][$field] = $value; - unset($data[$field]); - } - } - - return $data; - } - -/** - * Normalize `Xml::toArray()` to use in `Model::save()` - * - * @param array $xml XML as array - * @return array - */ - protected function _normalizeXmlData(array $xml) { - $return = array(); - foreach ($xml as $key => $value) { - if (is_array($value)) { - $return[Inflector::camelize($key)] = $this->_normalizeXmlData($value); - } elseif ($key[0] === '@') { - $return[substr($key, 1)] = $value; - } else { - $return[$key] = $value; - } - } - - return $return; - } - -/** - * Deconstructs a complex data type (array or object) into a single field value. - * - * @param string $field The name of the field to be deconstructed - * @param array|object $data An array or object to be deconstructed into a field - * @return mixed The resulting data that should be assigned to a field - */ - public function deconstruct($field, $data) { - if (!is_array($data)) { - return $data; - } - - $type = $this->getColumnType($field); - - if (!in_array($type, array('datetime', 'timestamp', 'date', 'time'))) { - return $data; - } - - $useNewDate = (isset($data['year']) || isset($data['month']) || - isset($data['day']) || isset($data['hour']) || isset($data['minute'])); - - $dateFields = array('Y' => 'year', 'm' => 'month', 'd' => 'day', 'H' => 'hour', 'i' => 'min', 's' => 'sec'); - $timeFields = array('H' => 'hour', 'i' => 'min', 's' => 'sec'); - $date = array(); - - if (isset($data['meridian']) && empty($data['meridian'])) { - return null; - } - - if (isset($data['hour']) && - isset($data['meridian']) && - !empty($data['hour']) && - $data['hour'] != 12 && - $data['meridian'] === 'pm' - ) { - $data['hour'] = $data['hour'] + 12; - } - - if (isset($data['hour']) && isset($data['meridian']) && $data['hour'] == 12 && $data['meridian'] === 'am') { - $data['hour'] = '00'; - } - - if ($type === 'time') { - foreach ($timeFields as $key => $val) { - if (!isset($data[$val]) || $data[$val] === '0' || $data[$val] === '00') { - $data[$val] = '00'; - } elseif ($data[$val] !== '') { - $data[$val] = sprintf('%02d', $data[$val]); - } - - if (!empty($data[$val])) { - $date[$key] = $data[$val]; - } else { - return null; - } - } - } - - if ($type === 'datetime' || $type === 'timestamp' || $type === 'date') { - foreach ($dateFields as $key => $val) { - if ($val === 'hour' || $val === 'min' || $val === 'sec') { - if (!isset($data[$val]) || $data[$val] === '0' || $data[$val] === '00') { - $data[$val] = '00'; - } else { - $data[$val] = sprintf('%02d', $data[$val]); - } - } - - if (!isset($data[$val]) || isset($data[$val]) && (empty($data[$val]) || substr($data[$val], 0, 1) === '-')) { - return null; - } - - if (isset($data[$val]) && !empty($data[$val])) { - $date[$key] = $data[$val]; - } - } - } - - if ($useNewDate && !empty($date)) { - $format = $this->getDataSource()->columns[$type]['format']; - foreach (array('m', 'd', 'H', 'i', 's') as $index) { - if (isset($date[$index])) { - $date[$index] = sprintf('%02d', $date[$index]); - } - } - - return str_replace(array_keys($date), array_values($date), $format); - } - - return $data; - } - -/** - * Returns an array of table metadata (column names and types) from the database. - * $field => keys(type, null, default, key, length, extra) - * - * @param bool|string $field Set to true to reload schema, or a string to return a specific field - * @return array|null Array of table metadata - */ - public function schema($field = false) { - if ($this->useTable !== false && (!is_array($this->_schema) || $field === true)) { - $db = $this->getDataSource(); - $db->cacheSources = ($this->cacheSources && $db->cacheSources); - if (method_exists($db, 'describe')) { - $this->_schema = $db->describe($this); - } - } - - if (!is_string($field)) { - return $this->_schema; - } - - if (isset($this->_schema[$field])) { - return $this->_schema[$field]; - } - - return null; - } - -/** - * Returns an associative array of field names and column types. - * - * @return array Field types indexed by field name - */ - public function getColumnTypes() { - $columns = $this->schema(); - if (empty($columns)) { - trigger_error(__d('cake_dev', '(Model::getColumnTypes) Unable to build model field data. If you are using a model without a database table, try implementing schema()'), E_USER_WARNING); - } - - $cols = array(); - foreach ($columns as $field => $values) { - $cols[$field] = $values['type']; - } - - return $cols; - } - -/** - * Returns the column type of a column in the model. - * - * @param string $column The name of the model column - * @return string Column type - */ - public function getColumnType($column) { - $cols = $this->schema(); - if (isset($cols[$column]) && isset($cols[$column]['type'])) { - return $cols[$column]['type']; - } - - $db = $this->getDataSource(); - $model = null; - - $startQuote = isset($db->startQuote) ? $db->startQuote : null; - $endQuote = isset($db->endQuote) ? $db->endQuote : null; - $column = str_replace(array($startQuote, $endQuote), '', $column); - - if (strpos($column, '.')) { - list($model, $column) = explode('.', $column); - } - - if (isset($model) && $model != $this->alias && isset($this->{$model})) { - return $this->{$model}->getColumnType($column); - } - - if (isset($cols[$column]) && isset($cols[$column]['type'])) { - return $cols[$column]['type']; - } - - return null; - } - -/** - * Returns true if the supplied field exists in the model's database table. - * - * @param string|array $name Name of field to look for, or an array of names - * @param bool $checkVirtual checks if the field is declared as virtual - * @return mixed If $name is a string, returns a boolean indicating whether the field exists. - * If $name is an array of field names, returns the first field that exists, - * or false if none exist. - */ - public function hasField($name, $checkVirtual = false) { - if (is_array($name)) { - foreach ($name as $n) { - if ($this->hasField($n, $checkVirtual)) { - return $n; - } - } - - return false; - } - - if ($checkVirtual && !empty($this->virtualFields) && $this->isVirtualField($name)) { - return true; - } - - if (empty($this->_schema)) { - $this->schema(); - } - - if ($this->_schema) { - return isset($this->_schema[$name]); - } - - return false; - } - -/** - * Check that a method is callable on a model. This will check both the model's own methods, its - * inherited methods and methods that could be callable through behaviors. - * - * @param string $method The method to be called. - * @return bool True on method being callable. - */ - public function hasMethod($method) { - if (method_exists($this, $method)) { - return true; - } - - return $this->Behaviors->hasMethod($method); - } - -/** - * Returns true if the supplied field is a model Virtual Field - * - * @param string $field Name of field to look for - * @return bool indicating whether the field exists as a model virtual field. - */ - public function isVirtualField($field) { - if (empty($this->virtualFields) || !is_string($field)) { - return false; - } - - if (isset($this->virtualFields[$field])) { - return true; - } - - if (strpos($field, '.') !== false) { - list($model, $field) = explode('.', $field); - if ($model === $this->alias && isset($this->virtualFields[$field])) { - return true; - } - } - - return false; - } - -/** - * Returns the expression for a model virtual field - * - * @param string $field Name of field to look for - * @return mixed If $field is string expression bound to virtual field $field - * If $field is null, returns an array of all model virtual fields - * or false if none $field exist. - */ - public function getVirtualField($field = null) { - if (!$field) { - return empty($this->virtualFields) ? false : $this->virtualFields; - } - - if ($this->isVirtualField($field)) { - if (strpos($field, '.') !== false) { - list(, $field) = pluginSplit($field); - } - - return $this->virtualFields[$field]; - } - - return false; - } - -/** - * Initializes the model for writing a new record, loading the default values - * for those fields that are not defined in $data, and clearing previous validation errors. - * Especially helpful for saving data in loops. - * - * @param bool|array $data Optional data array to assign to the model after it is created. If null or false, - * schema data defaults are not merged. - * @param bool $filterKey If true, overwrites any primary key input with an empty value - * @return array The current Model::data; after merging $data and/or defaults from database - * @link https://book.cakephp.org/2.0/en/models/saving-your-data.html#model-create-array-data-array - */ - public function create($data = array(), $filterKey = false) { - $defaults = array(); - $this->id = false; - $this->data = array(); - $this->validationErrors = array(); - - if ($data !== null && $data !== false) { - $schema = (array)$this->schema(); - foreach ($schema as $field => $properties) { - if ($this->primaryKey !== $field && isset($properties['default']) && $properties['default'] !== '') { - $defaults[$field] = $properties['default']; - } - } - - $this->set($defaults); - $this->set($data); - } - - if ($filterKey) { - $this->set($this->primaryKey, false); - } - - return $this->data; - } - -/** - * This function is a convenient wrapper class to create(false) and, as the name suggests, clears the id, data, and validation errors. - * - * @return bool Always true upon success - * @see Model::create() - */ - public function clear() { - $this->create(false); - return true; - } - -/** - * Returns a list of fields from the database, and sets the current model - * data (Model::$data) with the record found. - * - * @param string|array $fields String of single field name, or an array of field names. - * @param int|string $id The ID of the record to read - * @return array|false Array of database fields, or false if not found - * @link https://book.cakephp.org/2.0/en/models/retrieving-your-data.html#model-read - */ - public function read($fields = null, $id = null) { - $this->validationErrors = array(); - - if ($id) { - $this->id = $id; - } - - $id = $this->id; - - if (is_array($this->id)) { - $id = $this->id[0]; - } - - if ($id !== null && $id !== false) { - $this->data = $this->find('first', array( - 'conditions' => array($this->alias . '.' . $this->primaryKey => $id), - 'fields' => $fields - )); - - return $this->data; - } - - return false; - } - -/** - * Returns the content of a single field given the supplied conditions, - * of the first record in the supplied order. - * - * @param string $name The name of the field to get. - * @param array $conditions SQL conditions (defaults to NULL). - * @param string|array $order SQL ORDER BY fragment. - * @return string|false Field content, or false if not found. - * @link https://book.cakephp.org/2.0/en/models/retrieving-your-data.html#model-field - */ - public function field($name, $conditions = null, $order = null) { - if ($conditions === null && !in_array($this->id, array(false, null), true)) { - $conditions = array($this->alias . '.' . $this->primaryKey => $this->id); - } - - $recursive = $this->recursive; - if ($this->recursive >= 1) { - $recursive = -1; - } - - $fields = $name; - $data = $this->find('first', compact('conditions', 'fields', 'order', 'recursive')); - if (!$data) { - return false; - } - - if (strpos($name, '.') === false) { - if (isset($data[$this->alias][$name])) { - return $data[$this->alias][$name]; - } - } else { - $name = explode('.', $name); - if (isset($data[$name[0]][$name[1]])) { - return $data[$name[0]][$name[1]]; - } - } - - if (isset($data[0]) && count($data[0]) > 0) { - return array_shift($data[0]); - } - } - -/** - * Saves the value of a single field to the database, based on the current - * model ID. - * - * @param string $name Name of the table field - * @param mixed $value Value of the field - * @param bool|array $validate Either a boolean, or an array. - * If a boolean, indicates whether or not to validate before saving. - * If an array, allows control of 'validate', 'callbacks' and 'counterCache' options. - * See Model::save() for details of each options. - * @return bool|array See Model::save() False on failure or an array of model data on success. - * @deprecated 3.0.0 To ease migration to the new major, do not use this method anymore. - * Stateful model usage will be removed. Use the existing save() methods instead. - * @see Model::save() - * @link https://book.cakephp.org/2.0/en/models/saving-your-data.html#model-savefield-string-fieldname-string-fieldvalue-validate-false - */ - public function saveField($name, $value, $validate = false) { - $id = $this->id; - $this->create(false); - - $options = array('validate' => $validate, 'fieldList' => array($name)); - if (is_array($validate)) { - $options = $validate + array('validate' => false, 'fieldList' => array($name)); - } - - return $this->save(array($this->alias => array($this->primaryKey => $id, $name => $value)), $options); - } - -/** - * Saves model data (based on white-list, if supplied) to the database. By - * default, validation occurs before save. Passthrough method to _doSave() with - * transaction handling. - * - * @param array $data Data to save. - * @param bool|array $validate Either a boolean, or an array. - * If a boolean, indicates whether or not to validate before saving. - * If an array, can have following keys: - * - * - atomic: If true (default), will attempt to save the record in a single transaction. - * - validate: Set to true/false to enable or disable validation. - * - fieldList: An array of fields you want to allow for saving. - * - callbacks: Set to false to disable callbacks. Using 'before' or 'after' - * will enable only those callbacks. - * - `counterCache`: Boolean to control updating of counter caches (if any) - * - * @param array $fieldList List of fields to allow to be saved - * @return mixed On success Model::$data if its not empty or true, false on failure - * @throws Exception - * @throws PDOException - * @triggers Model.beforeSave $this, array($options) - * @triggers Model.afterSave $this, array($created, $options) - * @link https://book.cakephp.org/2.0/en/models/saving-your-data.html - */ - public function save($data = null, $validate = true, $fieldList = array()) { - $defaults = array( - 'validate' => true, 'fieldList' => array(), - 'callbacks' => true, 'counterCache' => true, - 'atomic' => true - ); - - if (!is_array($validate)) { - $options = compact('validate', 'fieldList') + $defaults; - } else { - $options = $validate + $defaults; - } - - if (!$options['atomic']) { - return $this->_doSave($data, $options); - } - - $db = $this->getDataSource(); - $transactionBegun = $db->begin(); - try { - $success = $this->_doSave($data, $options); - if ($transactionBegun) { - if ($success) { - $db->commit(); - } else { - $db->rollback(); - } - } - return $success; - } catch (Exception $e) { - if ($transactionBegun) { - $db->rollback(); - } - throw $e; - } - } - -/** - * Saves model data (based on white-list, if supplied) to the database. By - * default, validation occurs before save. - * - * @param array $data Data to save. - * @param array $options can have following keys: - * - * - validate: Set to true/false to enable or disable validation. - * - fieldList: An array of fields you want to allow for saving. - * - callbacks: Set to false to disable callbacks. Using 'before' or 'after' - * will enable only those callbacks. - * - `counterCache`: Boolean to control updating of counter caches (if any) - * - * @return mixed On success Model::$data if its not empty or true, false on failure - * @throws PDOException - * @link https://book.cakephp.org/2.0/en/models/saving-your-data.html - */ - protected function _doSave($data = null, $options = array()) { - $_whitelist = $this->whitelist; - $fields = array(); - - if (!empty($options['fieldList'])) { - if (!empty($options['fieldList'][$this->alias]) && is_array($options['fieldList'][$this->alias])) { - $this->whitelist = $options['fieldList'][$this->alias]; - } elseif (Hash::dimensions($options['fieldList']) < 2) { - $this->whitelist = $options['fieldList']; - } - } elseif ($options['fieldList'] === null) { - $this->whitelist = array(); - } - - $this->set($data); - - if (empty($this->data) && !$this->hasField(array('created', 'updated', 'modified'))) { - $this->whitelist = $_whitelist; - return false; - } - - foreach (array('created', 'updated', 'modified') as $field) { - $keyPresentAndEmpty = ( - isset($this->data[$this->alias]) && - array_key_exists($field, $this->data[$this->alias]) && - $this->data[$this->alias][$field] === null - ); - - if ($keyPresentAndEmpty) { - unset($this->data[$this->alias][$field]); - } - } - - $exists = $this->exists($this->getID()); - $dateFields = array('modified', 'updated'); - - if (!$exists) { - $dateFields[] = 'created'; - } - - if (isset($this->data[$this->alias])) { - $fields = array_keys($this->data[$this->alias]); - } - - if ($options['validate'] && !$this->validates($options)) { - $this->whitelist = $_whitelist; - return false; - } - - $db = $this->getDataSource(); - $now = time(); - - foreach ($dateFields as $updateCol) { - $fieldHasValue = in_array($updateCol, $fields); - $fieldInWhitelist = ( - count($this->whitelist) === 0 || - in_array($updateCol, $this->whitelist) - ); - if (($fieldHasValue && $fieldInWhitelist) || !$this->hasField($updateCol)) { - continue; - } - - $default = array('formatter' => 'date'); - $colType = array_merge($default, $db->columns[$this->getColumnType($updateCol)]); - - $time = $now; - if (array_key_exists('format', $colType)) { - $time = call_user_func($colType['formatter'], $colType['format']); - } - - if (!empty($this->whitelist)) { - $this->whitelist[] = $updateCol; - } - $this->set($updateCol, $time); - } - - if ($options['callbacks'] === true || $options['callbacks'] === 'before') { - $event = new CakeEvent('Model.beforeSave', $this, array($options)); - list($event->break, $event->breakOn) = array(true, array(false, null)); - $this->getEventManager()->dispatch($event); - if (!$event->result) { - $this->whitelist = $_whitelist; - return false; - } - } - - if (empty($this->data[$this->alias][$this->primaryKey])) { - unset($this->data[$this->alias][$this->primaryKey]); - } - $joined = $fields = $values = array(); - - foreach ($this->data as $n => $v) { - if (isset($this->hasAndBelongsToMany[$n])) { - if (isset($v[$n])) { - $v = $v[$n]; - } - $joined[$n] = $v; - } elseif ($n === $this->alias) { - foreach (array('created', 'updated', 'modified') as $field) { - if (array_key_exists($field, $v) && empty($v[$field])) { - unset($v[$field]); - } - } - - foreach ($v as $x => $y) { - if ($this->hasField($x) && (empty($this->whitelist) || in_array($x, $this->whitelist))) { - list($fields[], $values[]) = array($x, $y); - } - } - } - } - - if (empty($fields) && empty($joined)) { - $this->whitelist = $_whitelist; - return false; - } - - $count = count($fields); - - if (!$exists && $count > 0) { - $this->id = false; - } - - $success = true; - $created = false; - - if ($count > 0) { - $cache = $this->_prepareUpdateFields(array_combine($fields, $values)); - - if (!empty($this->id)) { - $this->__safeUpdateMode = true; - try { - $success = (bool)$db->update($this, $fields, $values); - } catch (Exception $e) { - $this->__safeUpdateMode = false; - throw $e; - } - $this->__safeUpdateMode = false; - } else { - if (empty($this->data[$this->alias][$this->primaryKey]) && $this->_isUUIDField($this->primaryKey)) { - if (array_key_exists($this->primaryKey, $this->data[$this->alias])) { - $j = array_search($this->primaryKey, $fields); - $values[$j] = CakeText::uuid(); - } else { - list($fields[], $values[]) = array($this->primaryKey, CakeText::uuid()); - } - } - - if (!$db->create($this, $fields, $values)) { - $success = false; - } else { - $created = true; - } - } - - if ($success && $options['counterCache'] && !empty($this->belongsTo)) { - $this->updateCounterCache($cache, $created); - } - } - - if ($success && !empty($joined)) { - $this->_saveMulti($joined, $this->id, $db); - } - - if (!$success) { - $this->whitelist = $_whitelist; - return $success; - } - - if ($count > 0) { - if ($created) { - $this->data[$this->alias][$this->primaryKey] = $this->id; - } - - if ($options['callbacks'] === true || $options['callbacks'] === 'after') { - $event = new CakeEvent('Model.afterSave', $this, array($created, $options)); - $this->getEventManager()->dispatch($event); - } - } - - if (!empty($this->data)) { - $success = $this->data; - } - - $this->_clearCache(); - $this->validationErrors = array(); - $this->whitelist = $_whitelist; - $this->data = false; - - return $success; - } - -/** - * Check if the passed in field is a UUID field - * - * @param string $field the field to check - * @return bool - */ - protected function _isUUIDField($field) { - $field = $this->schema($field); - return $field !== null && $field['length'] == 36 && in_array($field['type'], array('string', 'binary', 'uuid')); - } - -/** - * Saves model hasAndBelongsToMany data to the database. - * - * @param array $joined Data to save - * @param int|string $id ID of record in this model - * @param DataSource $db Datasource instance. - * @return void - */ - protected function _saveMulti($joined, $id, $db) { - foreach ($joined as $assoc => $data) { - if (!isset($this->hasAndBelongsToMany[$assoc])) { - continue; - } - - $habtm = $this->hasAndBelongsToMany[$assoc]; - - list($join) = $this->joinModel($habtm['with']); - - $Model = $this->{$join}; - - if (!empty($habtm['with'])) { - $withModel = is_array($habtm['with']) ? key($habtm['with']) : $habtm['with']; - list(, $withModel) = pluginSplit($withModel); - $dbMulti = $this->{$withModel}->getDataSource(); - } else { - $dbMulti = $db; - } - - $isUUID = !empty($Model->primaryKey) && $Model->_isUUIDField($Model->primaryKey); - - $newData = $newValues = $newJoins = array(); - $primaryAdded = false; - - $fields = array( - $dbMulti->name($habtm['foreignKey']), - $dbMulti->name($habtm['associationForeignKey']) - ); - - $idField = $db->name($Model->primaryKey); - if ($isUUID && !in_array($idField, $fields)) { - $fields[] = $idField; - $primaryAdded = true; - } - - foreach ((array)$data as $row) { - if ((is_string($row) && (strlen($row) === 36 || strlen($row) === 16)) || is_numeric($row)) { - $newJoins[] = $row; - $values = array($id, $row); - - if ($isUUID && $primaryAdded) { - $values[] = CakeText::uuid(); - } - - $newValues[$row] = $values; - unset($values); - } elseif (isset($row[$habtm['associationForeignKey']])) { - if (!empty($row[$Model->primaryKey])) { - $newJoins[] = $row[$habtm['associationForeignKey']]; - } - - $newData[] = $row; - } elseif (isset($row[$join]) && isset($row[$join][$habtm['associationForeignKey']])) { - if (!empty($row[$join][$Model->primaryKey])) { - $newJoins[] = $row[$join][$habtm['associationForeignKey']]; - } - - $newData[] = $row[$join]; - } - } - - $keepExisting = $habtm['unique'] === 'keepExisting'; - if ($habtm['unique']) { - $conditions = array( - $join . '.' . $habtm['foreignKey'] => $id - ); - - if (!empty($habtm['conditions'])) { - $conditions = array_merge($conditions, (array)$habtm['conditions']); - } - - $associationForeignKey = $Model->alias . '.' . $habtm['associationForeignKey']; - $links = $Model->find('all', array( - 'conditions' => $conditions, - 'recursive' => empty($habtm['conditions']) ? -1 : 0, - 'fields' => $associationForeignKey, - )); - - $oldLinks = Hash::extract($links, "{n}.{$associationForeignKey}"); - if (!empty($oldLinks)) { - if ($keepExisting && !empty($newJoins)) { - $conditions[$associationForeignKey] = array_diff($oldLinks, $newJoins); - } else { - $conditions[$associationForeignKey] = $oldLinks; - } - - $dbMulti->delete($Model, $conditions); - } - } - - if (!empty($newData)) { - foreach ($newData as $data) { - $data[$habtm['foreignKey']] = $id; - if (empty($data[$Model->primaryKey])) { - $Model->create(); - } - - $Model->save($data, array('atomic' => false)); - } - } - - if (!empty($newValues)) { - if ($keepExisting && !empty($links)) { - foreach ($links as $link) { - $oldJoin = $link[$join][$habtm['associationForeignKey']]; - if (!in_array($oldJoin, $newJoins)) { - $conditions[$associationForeignKey] = $oldJoin; - $db->delete($Model, $conditions); - } else { - unset($newValues[$oldJoin]); - } - } - - $newValues = array_values($newValues); - } - - if (!empty($newValues)) { - $dbMulti->insertMulti($Model, $fields, $newValues); - } - } - } - } - -/** - * Updates the counter cache of belongsTo associations after a save or delete operation - * - * @param array $keys Optional foreign key data, defaults to the information $this->data - * @param bool $created True if a new record was created, otherwise only associations with - * 'counterScope' defined get updated - * @return void - */ - public function updateCounterCache($keys = array(), $created = false) { - if (empty($keys) && isset($this->data[$this->alias])) { - $keys = $this->data[$this->alias]; - } - $keys['old'] = isset($keys['old']) ? $keys['old'] : array(); - - foreach ($this->belongsTo as $parent => $assoc) { - if (empty($assoc['counterCache'])) { - continue; - } - - $Model = $this->{$parent}; - - if (!is_array($assoc['counterCache'])) { - if (isset($assoc['counterScope'])) { - $assoc['counterCache'] = array($assoc['counterCache'] => $assoc['counterScope']); - } else { - $assoc['counterCache'] = array($assoc['counterCache'] => array()); - } - } - - $foreignKey = $assoc['foreignKey']; - $fkQuoted = $this->escapeField($assoc['foreignKey']); - - foreach ($assoc['counterCache'] as $field => $conditions) { - if (!is_string($field)) { - $field = Inflector::underscore($this->alias) . '_count'; - } - - if (!$Model->hasField($field)) { - continue; - } - - if ($conditions === true) { - $conditions = array(); - } else { - $conditions = (array)$conditions; - } - - if (!array_key_exists($foreignKey, $keys)) { - $keys[$foreignKey] = $this->field($foreignKey); - } - - $recursive = (empty($conditions) ? -1 : 0); - - if (isset($keys['old'][$foreignKey]) && $keys['old'][$foreignKey] != $keys[$foreignKey]) { - $conditions[$fkQuoted] = $keys['old'][$foreignKey]; - $count = (int)$this->find('count', compact('conditions', 'recursive')); - - $Model->updateAll( - array($field => $count), - array($Model->escapeField() => $keys['old'][$foreignKey]) - ); - } - - $conditions[$fkQuoted] = $keys[$foreignKey]; - - if ($recursive === 0) { - $conditions = array_merge($conditions, (array)$conditions); - } - - $count = (int)$this->find('count', compact('conditions', 'recursive')); - - $Model->updateAll( - array($field => $count), - array($Model->escapeField() => $keys[$foreignKey]) - ); - } - } - } - -/** - * Helper method for `Model::updateCounterCache()`. Checks the fields to be updated for - * - * @param array $data The fields of the record that will be updated - * @return array Returns updated foreign key values, along with an 'old' key containing the old - * values, or empty if no foreign keys are updated. - */ - protected function _prepareUpdateFields($data) { - $foreignKeys = array(); - foreach ($this->belongsTo as $assoc => $info) { - if (isset($info['counterCache']) && $info['counterCache']) { - $foreignKeys[$assoc] = $info['foreignKey']; - } - } - - $included = array_intersect($foreignKeys, array_keys($data)); - - if (empty($included) || empty($this->id)) { - return array(); - } - - $old = $this->find('first', array( - 'conditions' => array($this->alias . '.' . $this->primaryKey => $this->id), - 'fields' => array_values($included), - 'recursive' => -1 - )); - - return array_merge($data, array('old' => $old[$this->alias])); - } - -/** - * Backwards compatible passthrough method for: - * saveMany(), validateMany(), saveAssociated() and validateAssociated() - * - * Saves multiple individual records for a single model; Also works with a single record, as well as - * all its associated records. - * - * #### Options - * - * - `validate`: Set to false to disable validation, true to validate each record before saving, - * 'first' to validate *all* records before any are saved (default), - * or 'only' to only validate the records, but not save them. - * - `atomic`: If true (default), will attempt to save all records in a single transaction. - * Should be set to false if database/table does not support transactions. - * - `fieldList`: Equivalent to the $fieldList parameter in Model::save(). - * It should be an associate array with model name as key and array of fields as value. Eg. - * ``` - * array( - * 'SomeModel' => array('field'), - * 'AssociatedModel' => array('field', 'otherfield') - * ) - * ``` - * - `deep`: See saveMany/saveAssociated - * - `callbacks`: See Model::save() - * - `counterCache`: See Model::save() - * - * @param array $data Record data to save. This can be either a numerically-indexed array (for saving multiple - * records of the same type), or an array indexed by association name. - * @param array $options Options to use when saving record data, See $options above. - * @return mixed If atomic: True on success, or false on failure. - * Otherwise: array similar to the $data array passed, but values are set to true/false - * depending on whether each record saved successfully. - * @link https://book.cakephp.org/2.0/en/models/saving-your-data.html#model-saveassociated-array-data-null-array-options-array - * @link https://book.cakephp.org/2.0/en/models/saving-your-data.html#model-saveall-array-data-null-array-options-array - */ - public function saveAll($data = array(), $options = array()) { - $options += array('validate' => 'first'); - if (Hash::numeric(array_keys($data))) { - if ($options['validate'] === 'only') { - return $this->validateMany($data, $options); - } - - return $this->saveMany($data, $options); - } - - if ($options['validate'] === 'only') { - return $this->validateAssociated($data, $options); - } - - return $this->saveAssociated($data, $options); - } - -/** - * Saves multiple individual records for a single model - * - * #### Options - * - * - `validate`: Set to false to disable validation, true to validate each record before saving, - * 'first' to validate *all* records before any are saved (default), - * - `atomic`: If true (default), will attempt to save all records in a single transaction. - * Should be set to false if database/table does not support transactions. - * - `fieldList`: Equivalent to the $fieldList parameter in Model::save() - * - `deep`: If set to true, all associated data will be saved as well. - * - `callbacks`: See Model::save() - * - `counterCache`: See Model::save() - * - * @param array $data Record data to save. This should be a numerically-indexed array - * @param array $options Options to use when saving record data, See $options above. - * @return mixed If atomic: True on success, or false on failure. - * Otherwise: array similar to the $data array passed, but values are set to true/false - * depending on whether each record saved successfully. - * @throws PDOException - * @link https://book.cakephp.org/2.0/en/models/saving-your-data.html#model-savemany-array-data-null-array-options-array - */ - public function saveMany($data = null, $options = array()) { - if (empty($data)) { - $data = $this->data; - } - - $options += array('validate' => 'first', 'atomic' => true, 'deep' => false); - $this->validationErrors = $validationErrors = array(); - - if (empty($data) && $options['validate'] !== false) { - $result = $this->save($data, $options); - if (!$options['atomic']) { - return array(!empty($result)); - } - - return !empty($result); - } - - if ($options['validate'] === 'first') { - $validates = $this->validateMany($data, $options); - if ((!$validates && $options['atomic']) || (!$options['atomic'] && in_array(false, $validates, true))) { - return $validates; - } - $options['validate'] = false; - } - - $transactionBegun = false; - if ($options['atomic']) { - $db = $this->getDataSource(); - $transactionBegun = $db->begin(); - } - - try { - $return = array(); - foreach ($data as $key => $record) { - $validates = $this->create(null) !== null; - $saved = false; - if ($validates) { - if ($options['deep']) { - $saved = $this->saveAssociated($record, array('atomic' => false) + $options); - } else { - $saved = (bool)$this->save($record, array('atomic' => false) + $options); - } - } - - $validates = ($validates && ($saved === true || (is_array($saved) && !in_array(false, Hash::flatten($saved), true)))); - if (!$validates) { - $validationErrors[$key] = $this->validationErrors; - } - - if (!$options['atomic']) { - $return[$key] = $validates; - } elseif (!$validates) { - break; - } - } - - $this->validationErrors = $validationErrors; - - if (!$options['atomic']) { - return $return; - } - - if ($validates) { - if ($transactionBegun) { - return $db->commit() !== false; - } - return true; - } - - if ($transactionBegun) { - $db->rollback(); - } - return false; - } catch (Exception $e) { - if ($transactionBegun) { - $db->rollback(); - } - throw $e; - } - } - -/** - * Validates multiple individual records for a single model - * - * #### Options - * - * - `atomic`: If true (default), returns boolean. If false returns array. - * - `fieldList`: Equivalent to the $fieldList parameter in Model::save() - * - `deep`: If set to true, all associated data will be validated as well. - * - * Warning: This method could potentially change the passed argument `$data`, - * If you do not want this to happen, make a copy of `$data` before passing it - * to this method - * - * @param array &$data Record data to validate. This should be a numerically-indexed array - * @param array $options Options to use when validating record data (see above), See also $options of validates(). - * @return bool|array If atomic: True on success, or false on failure. - * Otherwise: array similar to the $data array passed, but values are set to true/false - * depending on whether each record validated successfully. - */ - public function validateMany(&$data, $options = array()) { - return $this->validator()->validateMany($data, $options); - } - -/** - * Saves a single record, as well as all its directly associated records. - * - * #### Options - * - * - `validate`: Set to `false` to disable validation, `true` to validate each record before saving, - * 'first' to validate *all* records before any are saved(default), - * - `atomic`: If true (default), will attempt to save all records in a single transaction. - * Should be set to false if database/table does not support transactions. - * - `fieldList`: Equivalent to the $fieldList parameter in Model::save(). - * It should be an associate array with model name as key and array of fields as value. Eg. - * ``` - * array( - * 'SomeModel' => array('field'), - * 'AssociatedModel' => array('field', 'otherfield') - * ) - * ``` - * - `deep`: If set to true, not only directly associated data is saved, but deeper nested associated data as well. - * - `callbacks`: See Model::save() - * - `counterCache`: See Model::save() - * - * @param array $data Record data to save. This should be an array indexed by association name. - * @param array $options Options to use when saving record data, See $options above. - * @return mixed If atomic: True on success, or false on failure. - * Otherwise: array similar to the $data array passed, but values are set to true/false - * depending on whether each record saved successfully. - * @throws PDOException - * @link https://book.cakephp.org/2.0/en/models/saving-your-data.html#model-saveassociated-array-data-null-array-options-array - */ - public function saveAssociated($data = null, $options = array()) { - if (empty($data)) { - $data = $this->data; - } - - $options += array('validate' => 'first', 'atomic' => true, 'deep' => false); - $this->validationErrors = $validationErrors = array(); - - if (empty($data) && $options['validate'] !== false) { - $result = $this->save($data, $options); - if (!$options['atomic']) { - return array(!empty($result)); - } - - return !empty($result); - } - - if ($options['validate'] === 'first') { - $validates = $this->validateAssociated($data, $options); - if ((!$validates && $options['atomic']) || (!$options['atomic'] && in_array(false, Hash::flatten($validates), true))) { - return $validates; - } - - $options['validate'] = false; - } - - $transactionBegun = false; - if ($options['atomic']) { - $db = $this->getDataSource(); - $transactionBegun = $db->begin(); - } - - try { - $associations = $this->getAssociated(); - $return = array(); - $validates = true; - foreach ($data as $association => $values) { - $isEmpty = empty($values) || (isset($values[$association]) && empty($values[$association])); - if ($isEmpty || !isset($associations[$association]) || $associations[$association] !== 'belongsTo') { - continue; - } - - $Model = $this->{$association}; - - $validates = $Model->create(null) !== null; - $saved = false; - if ($validates) { - if ($options['deep']) { - $saved = $Model->saveAssociated($values, array('atomic' => false) + $options); - } else { - $saved = (bool)$Model->save($values, array('atomic' => false) + $options); - } - $validates = ($saved === true || (is_array($saved) && !in_array(false, Hash::flatten($saved), true))); - } - - if ($validates) { - $key = $this->belongsTo[$association]['foreignKey']; - if (isset($data[$this->alias])) { - $data[$this->alias][$key] = $Model->id; - } else { - $data = array_merge(array($key => $Model->id), $data, array($key => $Model->id)); - } - $options = $this->_addToWhiteList($key, $options); - } else { - $validationErrors[$association] = $Model->validationErrors; - } - - $return[$association] = $validates; - } - - if ($validates && !($this->create(null) !== null && $this->save($data, array('atomic' => false) + $options))) { - $validationErrors[$this->alias] = $this->validationErrors; - $validates = false; - } - $return[$this->alias] = $validates; - - foreach ($data as $association => $values) { - if (!$validates) { - break; - } - - $isEmpty = empty($values) || (isset($values[$association]) && empty($values[$association])); - if ($isEmpty || !isset($associations[$association])) { - continue; - } - - $Model = $this->{$association}; - - $type = $associations[$association]; - $key = $this->{$type}[$association]['foreignKey']; - switch ($type) { - case 'hasOne': - if (isset($values[$association])) { - $values[$association][$key] = $this->id; - } else { - $values = array_merge(array($key => $this->id), $values, array($key => $this->id)); - } - - $validates = $Model->create(null) !== null; - $saved = false; - - if ($validates) { - $options = $Model->_addToWhiteList($key, $options); - if ($options['deep']) { - $saved = $Model->saveAssociated($values, array('atomic' => false) + $options); - } else { - $saved = (bool)$Model->save($values, $options); - } - } - - $validates = ($validates && ($saved === true || (is_array($saved) && !in_array(false, Hash::flatten($saved), true)))); - if (!$validates) { - $validationErrors[$association] = $Model->validationErrors; - } - - $return[$association] = $validates; - break; - case 'hasMany': - foreach ($values as $i => $value) { - if (isset($values[$i][$association])) { - $values[$i][$association][$key] = $this->id; - } else { - $values[$i] = array_merge(array($key => $this->id), $value, array($key => $this->id)); - } - } - - $options = $Model->_addToWhiteList($key, $options); - $_return = $Model->saveMany($values, array('atomic' => false) + $options); - if (in_array(false, $_return, true)) { - $validationErrors[$association] = $Model->validationErrors; - $validates = false; - } - - $return[$association] = $_return; - break; - } - } - $this->validationErrors = $validationErrors; - - if (isset($validationErrors[$this->alias])) { - $this->validationErrors = $validationErrors[$this->alias]; - unset($validationErrors[$this->alias]); - $this->validationErrors = array_merge($this->validationErrors, $validationErrors); - } - - if (!$options['atomic']) { - return $return; - } - if ($validates) { - if ($transactionBegun) { - return $db->commit() !== false; - } - - return true; - } - - if ($transactionBegun) { - $db->rollback(); - } - return false; - } catch (Exception $e) { - if ($transactionBegun) { - $db->rollback(); - } - throw $e; - } - } - -/** - * Helper method for saveAll() and friends, to add foreign key to fieldlist - * - * @param string $key fieldname to be added to list - * @param array $options Options list - * @return array options - */ - protected function _addToWhiteList($key, $options) { - if (empty($options['fieldList']) && $this->whitelist && !in_array($key, $this->whitelist)) { - $options['fieldList'][$this->alias] = $this->whitelist; - $options['fieldList'][$this->alias][] = $key; - return $options; - } - - if (!empty($options['fieldList'][$this->alias]) && is_array($options['fieldList'][$this->alias])) { - $options['fieldList'][$this->alias][] = $key; - return $options; - } - - if (!empty($options['fieldList']) && is_array($options['fieldList']) && Hash::dimensions($options['fieldList']) < 2) { - $options['fieldList'][] = $key; - } - - return $options; - } - -/** - * Validates a single record, as well as all its directly associated records. - * - * #### Options - * - * - `atomic`: If true (default), returns boolean. If false returns array. - * - `fieldList`: Equivalent to the $fieldList parameter in Model::save() - * - `deep`: If set to true, not only directly associated data , but deeper nested associated data is validated as well. - * - * Warning: This method could potentially change the passed argument `$data`, - * If you do not want this to happen, make a copy of `$data` before passing it - * to this method - * - * @param array &$data Record data to validate. This should be an array indexed by association name. - * @param array $options Options to use when validating record data (see above), See also $options of validates(). - * @return array|bool If atomic: True on success, or false on failure. - * Otherwise: array similar to the $data array passed, but values are set to true/false - * depending on whether each record validated successfully. - */ - public function validateAssociated(&$data, $options = array()) { - return $this->validator()->validateAssociated($data, $options); - } - -/** - * Updates multiple model records based on a set of conditions. - * - * @param array $fields Set of fields and values, indexed by fields. - * Fields are treated as SQL snippets, to insert literal values manually escape your data. - * @param mixed $conditions Conditions to match, true for all records - * @return bool True on success, false on failure - * @link https://book.cakephp.org/2.0/en/models/saving-your-data.html#model-updateall-array-fields-mixed-conditions - */ - public function updateAll($fields, $conditions = true) { - return $this->getDataSource()->update($this, $fields, null, $conditions); - } - -/** - * Removes record for given ID. If no ID is given, the current ID is used. Returns true on success. - * - * @param int|string $id ID of record to delete - * @param bool $cascade Set to true to delete records that depend on this record - * @return bool True on success - * @triggers Model.beforeDelete $this, array($cascade) - * @triggers Model.afterDelete $this - * @link https://book.cakephp.org/2.0/en/models/deleting-data.html - */ - public function delete($id = null, $cascade = true) { - if (!empty($id)) { - $this->id = $id; - } - - $id = $this->id; - - $event = new CakeEvent('Model.beforeDelete', $this, array($cascade)); - list($event->break, $event->breakOn) = array(true, array(false, null)); - $this->getEventManager()->dispatch($event); - if ($event->isStopped()) { - return false; - } - - if (!$this->exists($this->getID())) { - return false; - } - - $this->_deleteDependent($id, $cascade); - $this->_deleteLinks($id); - $this->id = $id; - - if (!empty($this->belongsTo)) { - foreach ($this->belongsTo as $assoc) { - if (empty($assoc['counterCache'])) { - continue; - } - - $keys = $this->find('first', array( - 'fields' => $this->_collectForeignKeys(), - 'conditions' => array($this->alias . '.' . $this->primaryKey => $id), - 'recursive' => -1, - 'callbacks' => false - )); - break; - } - } - - if (!$this->getDataSource()->delete($this, array($this->alias . '.' . $this->primaryKey => $id))) { - return false; - } - - if (!empty($keys[$this->alias])) { - $this->updateCounterCache($keys[$this->alias]); - } - - $this->getEventManager()->dispatch(new CakeEvent('Model.afterDelete', $this)); - $this->_clearCache(); - $this->id = false; - - return true; - } - -/** - * Cascades model deletes through associated hasMany and hasOne child records. - * - * @param string $id ID of record that was deleted - * @param bool $cascade Set to true to delete records that depend on this record - * @return void - */ - protected function _deleteDependent($id, $cascade) { - if ($cascade !== true) { - return; - } - - if (!empty($this->__backAssociation)) { - $savedAssociations = $this->__backAssociation; - $this->__backAssociation = array(); - } - - foreach (array_merge($this->hasMany, $this->hasOne) as $assoc => $data) { - if ($data['dependent'] !== true) { - continue; - } - - $Model = $this->{$assoc}; - - if ($data['foreignKey'] === false && $data['conditions'] && in_array($this->name, $Model->getAssociated('belongsTo'))) { - $Model->recursive = 0; - $conditions = array($this->escapeField(null, $this->name) => $id); - } else { - $Model->recursive = -1; - $conditions = array($Model->escapeField($data['foreignKey']) => $id); - if ($data['conditions']) { - $conditions = array_merge((array)$data['conditions'], $conditions); - } - } - - if (isset($data['exclusive']) && $data['exclusive']) { - $Model->deleteAll($conditions); - } else { - $records = $Model->find('all', array( - 'conditions' => $conditions, 'fields' => $Model->primaryKey - )); - - if (!empty($records)) { - foreach ($records as $record) { - $Model->delete($record[$Model->alias][$Model->primaryKey]); - } - } - } - } - - if (isset($savedAssociations)) { - $this->__backAssociation = $savedAssociations; - } - } - -/** - * Cascades model deletes through HABTM join keys. - * - * @param string $id ID of record that was deleted - * @return void - */ - protected function _deleteLinks($id) { - foreach ($this->hasAndBelongsToMany as $data) { - list(, $joinModel) = pluginSplit($data['with']); - $Model = $this->{$joinModel}; - $records = $Model->find('all', array( - 'conditions' => $this->_getConditionsForDeletingLinks($Model, $id, $data), - 'fields' => $Model->primaryKey, - 'recursive' => -1, - 'callbacks' => false - )); - - if (!empty($records)) { - foreach ($records as $record) { - $Model->delete($record[$Model->alias][$Model->primaryKey]); - } - } - } - } - -/** - * Returns the conditions to be applied to Model::find() when determining which HABTM records should be deleted via - * Model::_deleteLinks() - * - * @param Model $Model HABTM join model instance - * @param mixed $id The ID of the primary model which is being deleted - * @param array $relationshipConfig The relationship config defined on the primary model - * @return array - */ - protected function _getConditionsForDeletingLinks(Model $Model, $id, array $relationshipConfig) { - return array($Model->escapeField($relationshipConfig['foreignKey']) => $id); - } - -/** - * Deletes multiple model records based on a set of conditions. - * - * @param mixed $conditions Conditions to match - * @param bool $cascade Set to true to delete records that depend on this record - * @param bool $callbacks Run callbacks - * @return bool True on success, false on failure - * @link https://book.cakephp.org/2.0/en/models/deleting-data.html#deleteall - */ - public function deleteAll($conditions, $cascade = true, $callbacks = false) { - if (empty($conditions)) { - return false; - } - - $db = $this->getDataSource(); - - if (!$cascade && !$callbacks) { - return $db->delete($this, $conditions); - } - - $ids = $this->find('all', array_merge(array( - 'fields' => "{$this->alias}.{$this->primaryKey}", - 'order' => false, - 'group' => "{$this->alias}.{$this->primaryKey}", - 'recursive' => 0), compact('conditions')) - ); - - if ($ids === false || $ids === null) { - return false; - } - - $ids = Hash::extract($ids, "{n}.{$this->alias}.{$this->primaryKey}"); - if (empty($ids)) { - return true; - } - - if ($callbacks) { - $_id = $this->id; - $result = true; - foreach ($ids as $id) { - $result = $result && $this->delete($id, $cascade); - } - - $this->id = $_id; - return $result; - } - - foreach ($ids as $id) { - $this->_deleteLinks($id); - if ($cascade) { - $this->_deleteDependent($id, $cascade); - } - } - - return $db->delete($this, array($this->alias . '.' . $this->primaryKey => $ids)); - } - -/** - * Collects foreign keys from associations. - * - * @param string $type Association type. - * @return array - */ - protected function _collectForeignKeys($type = 'belongsTo') { - $result = array(); - - foreach ($this->{$type} as $assoc => $data) { - if (isset($data['foreignKey']) && is_string($data['foreignKey'])) { - $result[$assoc] = $data['foreignKey']; - } - } - - return $result; - } - -/** - * Returns true if a record with particular ID exists. - * - * If $id is not passed it calls `Model::getID()` to obtain the current record ID, - * and then performs a `Model::find('count')` on the currently configured datasource - * to ascertain the existence of the record in persistent storage. - * - * @param int|string $id ID of record to check for existence - * @return bool True if such a record exists - */ - public function exists($id = null) { - if ($id === null) { - $id = $this->getID(); - } - - if ($id === false) { - return false; - } - - if ($this->useTable === false) { - return false; - } - - return (bool)$this->find('count', array( - 'conditions' => array( - $this->alias . '.' . $this->primaryKey => $id - ), - 'recursive' => -1, - 'callbacks' => false - )); - } - -/** - * Returns true if a record that meets given conditions exists. - * - * @param array $conditions SQL conditions array - * @return bool True if such a record exists - */ - public function hasAny($conditions = null) { - return (bool)$this->find('count', array('conditions' => $conditions, 'recursive' => -1)); - } - -/** - * Queries the datasource and returns a result set array. - * - * Used to perform find operations, where the first argument is type of find operation to perform - * (all / first / count / neighbors / list / threaded), - * second parameter options for finding (indexed array, including: 'conditions', 'limit', - * 'recursive', 'page', 'fields', 'offset', 'order', 'callbacks') - * - * Eg: - * ``` - * $model->find('all', array( - * 'conditions' => array('name' => 'Thomas Anderson'), - * 'fields' => array('name', 'email'), - * 'order' => 'field3 DESC', - * 'recursive' => 1, - * 'group' => 'type', - * 'callbacks' => false, - * )); - * ``` - * - * In addition to the standard query keys above, you can provide Datasource, and behavior specific - * keys. For example, when using a SQL based datasource you can use the joins key to specify additional - * joins that should be part of the query. - * - * ``` - * $model->find('all', array( - * 'conditions' => array('name' => 'Thomas Anderson'), - * 'joins' => array( - * array( - * 'alias' => 'Thought', - * 'table' => 'thoughts', - * 'type' => 'LEFT', - * 'conditions' => '`Thought`.`person_id` = `Person`.`id`' - * ) - * ) - * )); - * ``` - * - * ### Disabling callbacks - * - * The `callbacks` key allows you to disable or specify the callbacks that should be run. To - * disable beforeFind & afterFind callbacks set `'callbacks' => false` in your options. You can - * also set the callbacks option to 'before' or 'after' to enable only the specified callback. - * - * ### Adding new find types - * - * Behaviors and find types can also define custom finder keys which are passed into find(). - * See the documentation for custom find types - * (https://book.cakephp.org/2.0/en/models/retrieving-your-data.html#creating-custom-find-types) - * for how to implement custom find types. - * - * Specifying 'fields' for notation 'list': - * - * - If no fields are specified, then 'id' is used for key and 'model->displayField' is used for value. - * - If a single field is specified, 'id' is used for key and specified field is used for value. - * - If three fields are specified, they are used (in order) for key, value and group. - * - Otherwise, first and second fields are used for key and value. - * - * Note: find(list) + database views have issues with MySQL 5.0. Try upgrading to MySQL 5.1 if you - * have issues with database views. - * - * Note: find(count) has its own return values. - * - * @param string $type Type of find operation (all / first / count / neighbors / list / threaded) - * @param array $query Option fields (conditions / fields / joins / limit / offset / order / page / group / callbacks) - * @return array|int|null Array of records, int if the type is count, or Null on failure. - * @link https://book.cakephp.org/2.0/en/models/retrieving-your-data.html - */ - public function find($type = 'first', $query = array()) { - $this->findQueryType = $type; - $this->id = $this->getID(); - - $query = $this->buildQuery($type, $query); - if ($query === null) { - return null; - } - - return $this->_readDataSource($type, $query); - } - -/** - * Read from the datasource - * - * Model::_readDataSource() is used by all find() calls to read from the data source and can be overloaded to allow - * caching of datasource calls. - * - * ``` - * protected function _readDataSource($type, $query) { - * $cacheName = md5(json_encode($query) . json_encode($this->hasOne) . json_encode($this->belongsTo)); - * $cache = Cache::read($cacheName, 'cache-config-name'); - * if ($cache !== false) { - * return $cache; - * } - * - * $results = parent::_readDataSource($type, $query); - * Cache::write($cacheName, $results, 'cache-config-name'); - * return $results; - * } - * ``` - * - * @param string $type Type of find operation (all / first / count / neighbors / list / threaded) - * @param array $query Option fields (conditions / fields / joins / limit / offset / order / page / group / callbacks) - * @return array - */ - protected function _readDataSource($type, $query) { - $results = $this->getDataSource()->read($this, $query); - $this->resetAssociations(); - - if ($query['callbacks'] === true || $query['callbacks'] === 'after') { - $results = $this->_filterResults($results); - } - - $this->findQueryType = null; - - if ($this->findMethods[$type] === true) { - return $this->{'_find' . ucfirst($type)}('after', $query, $results); - } - } - -/** - * Builds the query array that is used by the data source to generate the query to fetch the data. - * - * @param string $type Type of find operation (all / first / count / neighbors / list / threaded) - * @param array $query Option fields (conditions / fields / joins / limit / offset / order / page / group / callbacks) - * @return array|null Query array or null if it could not be build for some reasons - * @triggers Model.beforeFind $this, array($query) - * @see Model::find() - */ - public function buildQuery($type = 'first', $query = array()) { - $query = array_merge( - array( - 'conditions' => null, 'fields' => null, 'joins' => array(), 'limit' => null, - 'offset' => null, 'order' => null, 'page' => 1, 'group' => null, 'callbacks' => true, - ), - (array)$query - ); - - if ($this->findMethods[$type] === true) { - $query = $this->{'_find' . ucfirst($type)}('before', $query); - } - - if (!is_numeric($query['page']) || (int)$query['page'] < 1) { - $query['page'] = 1; - } - - if ($query['page'] > 1 && !empty($query['limit'])) { - $query['offset'] = ($query['page'] - 1) * $query['limit']; - } - - if ($query['order'] === null && $this->order !== null) { - $query['order'] = $this->order; - } - - if (is_object($query['order'])) { - $query['order'] = array($query['order']); - } else { - $query['order'] = (array)$query['order']; - } - - if ($query['callbacks'] === true || $query['callbacks'] === 'before') { - $event = new CakeEvent('Model.beforeFind', $this, array($query)); - list($event->break, $event->breakOn, $event->modParams) = array(true, array(false, null), 0); - $this->getEventManager()->dispatch($event); - - if ($event->isStopped()) { - return null; - } - - $query = $event->result === true ? $event->data[0] : $event->result; - } - - return $query; - } - -/** - * Handles the before/after filter logic for find('all') operations. Only called by Model::find(). - * - * @param string $state Either "before" or "after" - * @param array $query Query. - * @param array $results Results. - * @return array - * @see Model::find() - */ - protected function _findAll($state, $query, $results = array()) { - if ($state === 'before') { - return $query; - } - - return $results; - } - -/** - * Handles the before/after filter logic for find('first') operations. Only called by Model::find(). - * - * @param string $state Either "before" or "after" - * @param array $query Query. - * @param array $results Results. - * @return array - * @see Model::find() - */ - protected function _findFirst($state, $query, $results = array()) { - if ($state === 'before') { - $query['limit'] = 1; - return $query; - } - - if (empty($results[0])) { - return array(); - } - - return $results[0]; - } - -/** - * Handles the before/after filter logic for find('count') operations. Only called by Model::find(). - * - * @param string $state Either "before" or "after" - * @param array $query Query. - * @param array $results Results. - * @return int|false The number of records found, or false - * @see Model::find() - */ - protected function _findCount($state, $query, $results = array()) { - if ($state === 'before') { - if (!empty($query['type']) && isset($this->findMethods[$query['type']]) && $query['type'] !== 'count') { - $query['operation'] = 'count'; - $query = $this->{'_find' . ucfirst($query['type'])}('before', $query); - } - - $db = $this->getDataSource(); - $query['order'] = false; - if (!method_exists($db, 'calculate')) { - return $query; - } - - if (!empty($query['fields']) && is_array($query['fields'])) { - if (!preg_match('/^count/i', current($query['fields']))) { - unset($query['fields']); - } - } - - if (empty($query['fields'])) { - $query['fields'] = $db->calculate($this, 'count'); - } elseif (method_exists($db, 'expression') && is_string($query['fields']) && !preg_match('/count/i', $query['fields'])) { - $query['fields'] = $db->calculate($this, 'count', array( - $db->expression($query['fields']), 'count' - )); - } - - return $query; - } - - foreach (array(0, $this->alias) as $key) { - if (isset($results[0][$key]['count'])) { - if ($query['group']) { - return count($results); - } - - return (int)$results[0][$key]['count']; - } - } - - return false; - } - -/** - * Handles the before/after filter logic for find('list') operations. Only called by Model::find(). - * - * @param string $state Either "before" or "after" - * @param array $query Query. - * @param array $results Results. - * @return array Key/value pairs of primary keys/display field values of all records found - * @see Model::find() - */ - protected function _findList($state, $query, $results = array()) { - if ($state === 'before') { - if (empty($query['fields'])) { - $query['fields'] = array("{$this->alias}.{$this->primaryKey}", "{$this->alias}.{$this->displayField}"); - $list = array("{n}.{$this->alias}.{$this->primaryKey}", "{n}.{$this->alias}.{$this->displayField}", null); - } else { - if (!is_array($query['fields'])) { - $query['fields'] = CakeText::tokenize($query['fields']); - } - - if (count($query['fields']) === 1) { - if (strpos($query['fields'][0], '.') === false) { - $query['fields'][0] = $this->alias . '.' . $query['fields'][0]; - } - - $list = array("{n}.{$this->alias}.{$this->primaryKey}", '{n}.' . $query['fields'][0], null); - $query['fields'] = array("{$this->alias}.{$this->primaryKey}", $query['fields'][0]); - } elseif (count($query['fields']) === 3) { - for ($i = 0; $i < 3; $i++) { - if (strpos($query['fields'][$i], '.') === false) { - $query['fields'][$i] = $this->alias . '.' . $query['fields'][$i]; - } - } - - $list = array('{n}.' . $query['fields'][0], '{n}.' . $query['fields'][1], '{n}.' . $query['fields'][2]); - } else { - for ($i = 0; $i < 2; $i++) { - if (strpos($query['fields'][$i], '.') === false) { - $query['fields'][$i] = $this->alias . '.' . $query['fields'][$i]; - } - } - - $list = array('{n}.' . $query['fields'][0], '{n}.' . $query['fields'][1], null); - } - } - - if (!isset($query['recursive']) || $query['recursive'] === null) { - $query['recursive'] = -1; - } - list($query['list']['keyPath'], $query['list']['valuePath'], $query['list']['groupPath']) = $list; - - return $query; - } - - if (empty($results)) { - return array(); - } - - return Hash::combine($results, $query['list']['keyPath'], $query['list']['valuePath'], $query['list']['groupPath']); - } - -/** - * Detects the previous field's value, then uses logic to find the 'wrapping' - * rows and return them. - * - * @param string $state Either "before" or "after" - * @param array $query Query. - * @param array $results Results. - * @return array - */ - protected function _findNeighbors($state, $query, $results = array()) { - extract($query); - - if ($state === 'before') { - $conditions = (array)$conditions; - if (isset($field) && isset($value)) { - if (strpos($field, '.') === false) { - $field = $this->alias . '.' . $field; - } - } else { - $field = $this->alias . '.' . $this->primaryKey; - $value = $this->id; - } - - $query['conditions'] = array_merge($conditions, array($field . ' <' => $value)); - $query['order'] = $field . ' DESC'; - $query['limit'] = 1; - $query['field'] = $field; - $query['value'] = $value; - - return $query; - } - - unset($query['conditions'][$field . ' <']); - $return = array(); - if (isset($results[0])) { - $prevVal = Hash::get($results[0], $field); - $query['conditions'][$field . ' >='] = $prevVal; - $query['conditions'][$field . ' !='] = $value; - $query['limit'] = 2; - } else { - $return['prev'] = null; - $query['conditions'][$field . ' >'] = $value; - $query['limit'] = 1; - } - - $query['order'] = $field . ' ASC'; - $neighbors = $this->find('all', $query); - if (!array_key_exists('prev', $return)) { - $return['prev'] = isset($neighbors[0]) ? $neighbors[0] : null; - } - - if (count($neighbors) === 2) { - $return['next'] = $neighbors[1]; - } elseif (count($neighbors) === 1 && !$return['prev']) { - $return['next'] = $neighbors[0]; - } else { - $return['next'] = null; - } - - return $return; - } - -/** - * In the event of ambiguous results returned (multiple top level results, with different parent_ids) - * top level results with different parent_ids to the first result will be dropped - * - * @param string $state Either "before" or "after". - * @param array $query Query. - * @param array $results Results. - * @return array Threaded results - */ - protected function _findThreaded($state, $query, $results = array()) { - if ($state === 'before') { - return $query; - } - - $parent = 'parent_id'; - if (isset($query['parent'])) { - $parent = $query['parent']; - } - - return Hash::nest($results, array( - 'idPath' => '{n}.' . $this->alias . '.' . $this->primaryKey, - 'parentPath' => '{n}.' . $this->alias . '.' . $parent - )); - } - -/** - * Passes query results through model and behavior afterFind() methods. - * - * @param array $results Results to filter - * @param bool $primary If this is the primary model results (results from model where the find operation was performed) - * @return array Set of filtered results - * @triggers Model.afterFind $this, array($results, $primary) - */ - protected function _filterResults($results, $primary = true) { - $event = new CakeEvent('Model.afterFind', $this, array($results, $primary)); - $event->modParams = 0; - $this->getEventManager()->dispatch($event); - return $event->result; - } - -/** - * This resets the association arrays for the model back - * to those originally defined in the model. Normally called at the end - * of each call to Model::find() - * - * @return bool Success - */ - public function resetAssociations() { - if (!empty($this->__backAssociation)) { - foreach ($this->_associations as $type) { - if (isset($this->__backAssociation[$type])) { - $this->{$type} = $this->__backAssociation[$type]; - } - } - - $this->__backAssociation = array(); - } - - foreach ($this->_associations as $type) { - foreach ($this->{$type} as $key => $name) { - if (property_exists($this, $key) && !empty($this->{$key}->__backAssociation)) { - $this->{$key}->resetAssociations(); - } - } - } - - $this->__backAssociation = array(); - return true; - } - -/** - * Returns false if any fields passed match any (by default, all if $or = false) of their matching values. - * - * Can be used as a validation method. When used as a validation method, the `$or` parameter - * contains an array of fields to be validated. - * - * @param array $fields Field/value pairs to search (if no values specified, they are pulled from $this->data) - * @param bool|array $or If false, all fields specified must match in order for a false return value - * @return bool False if any records matching any fields are found - */ - public function isUnique($fields, $or = true) { - if (is_array($or)) { - $isRule = ( - array_key_exists('rule', $or) && - array_key_exists('required', $or) && - array_key_exists('message', $or) - ); - if (!$isRule) { - $args = func_get_args(); - $fields = $args[1]; - $or = isset($args[2]) ? $args[2] : true; - } - } - if (!is_array($fields)) { - $fields = func_get_args(); - $fieldCount = count($fields) - 1; - if (is_bool($fields[$fieldCount])) { - $or = $fields[$fieldCount]; - unset($fields[$fieldCount]); - } - } - - foreach ($fields as $field => $value) { - if (is_numeric($field)) { - unset($fields[$field]); - - $field = $value; - $value = null; - if (isset($this->data[$this->alias][$field])) { - $value = $this->data[$this->alias][$field]; - } - } - - if (strpos($field, '.') === false) { - unset($fields[$field]); - $fields[$this->alias . '.' . $field] = $value; - } - } - - if ($or) { - $fields = array('or' => $fields); - } - - if (!empty($this->id)) { - $fields[$this->alias . '.' . $this->primaryKey . ' !='] = $this->id; - } - - return !$this->find('count', array('conditions' => $fields, 'recursive' => -1)); - } - -/** - * Returns a resultset for a given SQL statement. Custom SQL queries should be performed with this method. - * - * The method can options 2nd and 3rd parameters. - * - * - 2nd param: Either a boolean to control query caching or an array of parameters - * for use with prepared statement placeholders. - * - 3rd param: If 2nd argument is provided, a boolean flag for enabling/disabled - * query caching. - * - * If the query cache param as 2nd or 3rd argument is not given then the model's - * default `$cacheQueries` value is used. - * - * @param string $sql SQL statement - * @return mixed Resultset array or boolean indicating success / failure depending on the query executed - * @link https://book.cakephp.org/2.0/en/models/retrieving-your-data.html#model-query - */ - public function query($sql) { - $params = func_get_args(); - // use $this->cacheQueries as default when argument not explicitly given already - if (count($params) === 1 || count($params) === 2 && !is_bool($params[1])) { - $params[] = $this->cacheQueries; - } - $db = $this->getDataSource(); - return call_user_func_array(array(&$db, 'query'), $params); - } - -/** - * Returns true if all fields pass validation. Will validate hasAndBelongsToMany associations - * that use the 'with' key as well. Since _saveMulti is incapable of exiting a save operation. - * - * Will validate the currently set data. Use Model::set() or Model::create() to set the active data. - * - * @param array $options An optional array of custom options to be made available in the beforeValidate callback - * @return bool True if there are no errors - */ - public function validates($options = array()) { - return $this->validator()->validates($options); - } - -/** - * Returns an array of fields that have failed the validation of the current model. - * - * Additionally it populates the validationErrors property of the model with the same array. - * - * @param array|string $options An optional array of custom options to be made available in the beforeValidate callback - * @return array|bool Array of invalid fields and their error messages - * @see Model::validates() - */ - public function invalidFields($options = array()) { - return $this->validator()->errors($options); - } - -/** - * Marks a field as invalid, optionally setting the name of validation - * rule (in case of multiple validation for field) that was broken. - * - * @param string $field The name of the field to invalidate - * @param mixed $value Name of validation rule that was not failed, or validation message to - * be returned. If no validation key is provided, defaults to true. - * @return void - */ - public function invalidate($field, $value = true) { - $this->validator()->invalidate($field, $value); - } - -/** - * Returns true if given field name is a foreign key in this model. - * - * @param string $field Returns true if the input string ends in "_id" - * @return bool True if the field is a foreign key listed in the belongsTo array. - */ - public function isForeignKey($field) { - $foreignKeys = array(); - if (!empty($this->belongsTo)) { - foreach ($this->belongsTo as $data) { - $foreignKeys[] = $data['foreignKey']; - } - } - - return in_array($field, $foreignKeys); - } - -/** - * Escapes the field name and prepends the model name. Escaping is done according to the - * current database driver's rules. - * - * @param string $field Field to escape (e.g: id) - * @param string $alias Alias for the model (e.g: Post) - * @return string The name of the escaped field for this Model (i.e. id becomes `Post`.`id`). - */ - public function escapeField($field = null, $alias = null) { - if (empty($alias)) { - $alias = $this->alias; - } - - if (empty($field)) { - $field = $this->primaryKey; - } - - $db = $this->getDataSource(); - if (strpos($field, $db->name($alias) . '.') === 0) { - return $field; - } - - return $db->name($alias . '.' . $field); - } - -/** - * Returns the current record's ID - * - * @param int $list Index on which the composed ID is located - * @return mixed The ID of the current record, false if no ID - */ - public function getID($list = 0) { - if (empty($this->id) || (is_array($this->id) && isset($this->id[0]) && empty($this->id[0]))) { - return false; - } - - if (!is_array($this->id)) { - return $this->id; - } - - if (isset($this->id[$list]) && !empty($this->id[$list])) { - return $this->id[$list]; - } - - if (isset($this->id[$list])) { - return false; - } - - return current($this->id); - } - -/** - * Returns the ID of the last record this model inserted. - * - * @return mixed Last inserted ID - */ - public function getLastInsertID() { - return $this->getInsertID(); - } - -/** - * Returns the ID of the last record this model inserted. - * - * @return mixed Last inserted ID - */ - public function getInsertID() { - return $this->_insertID; - } - -/** - * Sets the ID of the last record this model inserted - * - * @param int|string $id Last inserted ID - * @return void - */ - public function setInsertID($id) { - $this->_insertID = $id; - } - -/** - * Returns the number of rows returned from the last query. - * - * @return int Number of rows - */ - public function getNumRows() { - return $this->getDataSource()->lastNumRows(); - } - -/** - * Returns the number of rows affected by the last query. - * - * @return int Number of rows - */ - public function getAffectedRows() { - return $this->getDataSource()->lastAffected(); - } - -/** - * Sets the DataSource to which this model is bound. - * - * @param string $dataSource The name of the DataSource, as defined in app/Config/database.php - * @return void - * @throws MissingConnectionException - */ - public function setDataSource($dataSource = null) { - $oldConfig = $this->useDbConfig; - - if ($dataSource) { - $this->useDbConfig = $dataSource; - } - - $db = ConnectionManager::getDataSource($this->useDbConfig); - if (!empty($oldConfig) && isset($db->config['prefix'])) { - $oldDb = ConnectionManager::getDataSource($oldConfig); - - if (!isset($this->tablePrefix) || (!isset($oldDb->config['prefix']) || $this->tablePrefix === $oldDb->config['prefix'])) { - $this->tablePrefix = $db->config['prefix']; - } - } elseif (isset($db->config['prefix'])) { - $this->tablePrefix = $db->config['prefix']; - } - - $schema = $db->getSchemaName(); - $defaultProperties = get_class_vars(get_class($this)); - if (isset($defaultProperties['schemaName'])) { - $schema = $defaultProperties['schemaName']; - } - $this->schemaName = $schema; - } - -/** - * Gets the DataSource to which this model is bound. - * - * @return DataSource A DataSource object - */ - public function getDataSource() { - if (!$this->_sourceConfigured && $this->useTable !== false) { - $this->_sourceConfigured = true; - $this->setSource($this->useTable); - } - - return ConnectionManager::getDataSource($this->useDbConfig); - } - -/** - * Get associations - * - * @return array - */ - public function associations() { - return $this->_associations; - } - -/** - * Gets all the models with which this model is associated. - * - * @param string $type Only result associations of this type - * @return array|null Associations - */ - public function getAssociated($type = null) { - if (!$type) { - $associated = array(); - foreach ($this->_associations as $assoc) { - if (!empty($this->{$assoc})) { - $models = array_keys($this->{$assoc}); - foreach ($models as $m) { - $associated[$m] = $assoc; - } - } - } - - return $associated; - } - - if (in_array($type, $this->_associations)) { - if (empty($this->{$type})) { - return array(); - } - - return array_keys($this->{$type}); - } - - $assoc = array_merge( - $this->hasOne, - $this->hasMany, - $this->belongsTo, - $this->hasAndBelongsToMany - ); - - if (array_key_exists($type, $assoc)) { - foreach ($this->_associations as $a) { - if (isset($this->{$a}[$type])) { - $assoc[$type]['association'] = $a; - break; - } - } - - return $assoc[$type]; - } - - return null; - } - -/** - * Gets the name and fields to be used by a join model. This allows specifying join fields - * in the association definition. - * - * @param string|array $assoc The model to be joined - * @param array $keys Any join keys which must be merged with the keys queried - * @return array - */ - public function joinModel($assoc, $keys = array()) { - if (is_string($assoc)) { - list(, $assoc) = pluginSplit($assoc); - return array($assoc, array_keys($this->{$assoc}->schema())); - } - - if (is_array($assoc)) { - $with = key($assoc); - return array($with, array_unique(array_merge($assoc[$with], $keys))); - } - - trigger_error( - __d('cake_dev', 'Invalid join model settings in %s. The association parameter has the wrong type, expecting a string or array, but was passed type: %s', $this->alias, gettype($assoc)), - E_USER_WARNING - ); - } - -/** - * Called before each find operation. Return false if you want to halt the find - * call, otherwise return the (modified) query data. - * - * @param array $query Data used to execute this query, i.e. conditions, order, etc. - * @return mixed true if the operation should continue, false if it should abort; or, modified - * $query to continue with new $query - * @link https://book.cakephp.org/2.0/en/models/callback-methods.html#beforefind - */ - public function beforeFind($query) { - return true; - } - -/** - * Called after each find operation. Can be used to modify any results returned by find(). - * Return value should be the (modified) results. - * - * @param mixed $results The results of the find operation - * @param bool $primary Whether this model is being queried directly (vs. being queried as an association) - * @return mixed Result of the find operation - * @link https://book.cakephp.org/2.0/en/models/callback-methods.html#afterfind - */ - public function afterFind($results, $primary = false) { - return $results; - } - -/** - * Called before each save operation, after validation. Return a non-true result - * to halt the save. - * - * @param array $options Options passed from Model::save(). - * @return bool True if the operation should continue, false if it should abort - * @link https://book.cakephp.org/2.0/en/models/callback-methods.html#beforesave - * @see Model::save() - */ - public function beforeSave($options = array()) { - return true; - } - -/** - * Called after each successful save operation. - * - * @param bool $created True if this save created a new record - * @param array $options Options passed from Model::save(). - * @return void - * @link https://book.cakephp.org/2.0/en/models/callback-methods.html#aftersave - * @see Model::save() - */ - public function afterSave($created, $options = array()) { - } - -/** - * Called before every deletion operation. - * - * @param bool $cascade If true records that depend on this record will also be deleted - * @return bool True if the operation should continue, false if it should abort - * @link https://book.cakephp.org/2.0/en/models/callback-methods.html#beforedelete - */ - public function beforeDelete($cascade = true) { - return true; - } - -/** - * Called after every deletion operation. - * - * @return void - * @link https://book.cakephp.org/2.0/en/models/callback-methods.html#afterdelete - */ - public function afterDelete() { - } - -/** - * Called during validation operations, before validation. Please note that custom - * validation rules can be defined in $validate. - * - * @param array $options Options passed from Model::save(). - * @return bool True if validate operation should continue, false to abort - * @link https://book.cakephp.org/2.0/en/models/callback-methods.html#beforevalidate - * @see Model::save() - */ - public function beforeValidate($options = array()) { - return true; - } - -/** - * Called after data has been checked for errors - * - * @return void - */ - public function afterValidate() { - } - -/** - * Called when a DataSource-level error occurs. - * - * @return void - * @link https://book.cakephp.org/2.0/en/models/callback-methods.html#onerror - */ - public function onError() { - } - -/** - * Clears cache for this model. - * - * @param string $type If null this deletes cached views if Cache.check is true - * Will be used to allow deleting query cache also - * @return mixed True on delete, null otherwise - */ - protected function _clearCache($type = null) { - if ($type !== null || Configure::read('Cache.check') !== true) { - return; - } - $pluralized = Inflector::pluralize($this->alias); - $assoc = array( - strtolower($pluralized), - Inflector::underscore($pluralized) - ); - foreach ($this->_associations as $association) { - foreach ($this->{$association} as $className) { - $pluralizedAssociation = Inflector::pluralize($className['className']); - if (!in_array(strtolower($pluralizedAssociation), $assoc)) { - $assoc = array_merge($assoc, array( - strtolower($pluralizedAssociation), - Inflector::underscore($pluralizedAssociation) - )); - } - } - } - clearCache(array_unique($assoc)); - return true; - } - -/** - * Returns an instance of a model validator for this class - * - * @param ModelValidator $instance Model validator instance. - * If null a new ModelValidator instance will be made using current model object - * @return ModelValidator - */ - public function validator(ModelValidator $instance = null) { - if ($instance) { - $this->_validator = $instance; - } elseif (!$this->_validator) { - $this->_validator = new ModelValidator($this); - } - - return $this->_validator; - } + /** + * Default list of association keys. + * + * @var array + */ + protected $_associationKeys = [ + 'belongsTo' => ['className', 'foreignKey', 'conditions', 'fields', 'order', 'counterCache'], + 'hasOne' => ['className', 'foreignKey', 'conditions', 'fields', 'order', 'dependent'], + 'hasMany' => ['className', 'foreignKey', 'conditions', 'fields', 'order', 'limit', 'offset', 'dependent', 'exclusive', 'finderQuery', 'counterQuery'], + 'hasAndBelongsToMany' => ['className', 'joinTable', 'with', 'foreignKey', 'associationForeignKey', 'conditions', 'fields', 'order', 'limit', 'offset', 'unique', 'finderQuery'] + ]; + /** + * Holds provided/generated association key names and other data for all associations. + * + * @var array + */ + protected $_associations = ['belongsTo', 'hasOne', 'hasMany', 'hasAndBelongsToMany']; + /** + * The ID of the model record that was last inserted. + * + * @var int|string + */ + protected $_insertID = null; + /** + * Has the datasource been configured. + * + * @var bool + * @see Model::getDataSource + */ + protected $_sourceConfigured = false; + /** + * Instance of the CakeEventManager this model is using + * to dispatch inner events. + * + * @var CakeEventManager + */ + protected $_eventManager = null; + + /** + * Instance of the ModelValidator + * + * @var ModelValidator + */ + protected $_validator = null; + + /** + * Constructor. Binds the model's database table to the object. + * + * If `$id` is an array it can be used to pass several options into the model. + * + * - `id`: The id to start the model on. + * - `table`: The table to use for this model. + * - `ds`: The connection name this model is connected to. + * - `name`: The name of the model eg. Post. + * - `alias`: The alias of the model, this is used for registering the instance in the `ClassRegistry`. + * eg. `ParentThread` + * + * ### Overriding Model's __construct method. + * + * When overriding Model::__construct() be careful to include and pass in all 3 of the + * arguments to `parent::__construct($id, $table, $ds);` + * + * ### Dynamically creating models + * + * You can dynamically create model instances using the $id array syntax. + * + * ``` + * $Post = new Model(array('table' => 'posts', 'name' => 'Post', 'ds' => 'connection2')); + * ``` + * + * Would create a model attached to the posts table on connection2. Dynamic model creation is useful + * when you want a model object that contains no associations or attached behaviors. + * + * @param bool|int|string|array $id Set this ID for this model on startup, + * can also be an array of options, see above. + * @param string|false $table Name of database table to use. + * @param string $ds DataSource connection name. + */ + public function __construct($id = false, $table = null, $ds = null) + { + parent::__construct(); + + if (is_array($id)) { + extract(array_merge( + [ + 'id' => $this->id, 'table' => $this->useTable, 'ds' => $this->useDbConfig, + 'name' => $this->name, 'alias' => $this->alias, 'plugin' => $this->plugin + ], + $id + )); + } + + if ($this->plugin === null) { + $this->plugin = (isset($plugin) ? $plugin : $this->plugin); + } + + if ($this->name === null) { + $this->name = (isset($name) ? $name : get_class($this)); + } + + if ($this->alias === null) { + $this->alias = (isset($alias) ? $alias : $this->name); + } + + if ($this->primaryKey === null) { + $this->primaryKey = 'id'; + } + + ClassRegistry::addObject($this->alias, $this); + + $this->id = $id; + unset($id); + + if ($table === false) { + $this->useTable = false; + } else if ($table) { + $this->useTable = $table; + } + + if ($ds !== null) { + $this->useDbConfig = $ds; + } + + if (is_subclass_of($this, 'AppModel')) { + $merge = ['actsAs', 'findMethods']; + $parentClass = get_parent_class($this); + if ($parentClass !== 'AppModel') { + $this->_mergeVars($merge, $parentClass); + } + $this->_mergeVars($merge, 'AppModel'); + } + $this->_mergeVars(['findMethods'], 'Model'); + + $this->Behaviors = new BehaviorCollection(); + + if ($this->useTable !== false) { + + if ($this->useTable === null) { + $this->useTable = Inflector::tableize($this->name); + } + + if (!$this->displayField) { + unset($this->displayField); + } + $this->table = $this->useTable; + $this->tableToModel[$this->table] = $this->alias; + } else if ($this->table === false) { + $this->table = Inflector::tableize($this->name); + } + + if ($this->tablePrefix === null) { + unset($this->tablePrefix); + } + + $this->_createLinks(); + $this->Behaviors->init($this->alias, $this->actsAs); + } + + /** + * Create a set of associations. + * + * @return void + */ + protected function _createLinks() + { + foreach ($this->_associations as $type) { + $association =& $this->{$type}; + + if (!is_array($association)) { + $association = explode(',', $association); + + foreach ($association as $i => $className) { + $className = trim($className); + unset ($association[$i]); + $association[$className] = []; + } + } + + if (!empty($association)) { + foreach ($association as $assoc => $value) { + $plugin = null; + + if (is_numeric($assoc)) { + unset($association[$assoc]); + $assoc = $value; + $value = []; + $association[$assoc] = $value; + } + + if (!isset($value['className']) && strpos($assoc, '.') !== false) { + unset($association[$assoc]); + list($plugin, $assoc) = pluginSplit($assoc, true); + $association[$assoc] = ['className' => $plugin . $assoc] + $value; + } + + $this->_generateAssociation($type, $assoc); + } + } + } + } + + /** + * Build an array-based association from string. + * + * @param string $type 'belongsTo', 'hasOne', 'hasMany', 'hasAndBelongsToMany' + * @param string $assocKey Association key. + * @return void + */ + protected function _generateAssociation($type, $assocKey) + { + $class = $assocKey; + $dynamicWith = false; + $assoc =& $this->{$type}[$assocKey]; + + foreach ($this->_associationKeys[$type] as $key) { + if (!isset($assoc[$key]) || $assoc[$key] === null) { + $data = ''; + + switch ($key) { + case 'fields': + $data = ''; + break; + + case 'foreignKey': + $data = (($type === 'belongsTo') ? Inflector::underscore($assocKey) : Inflector::singularize($this->table)) . '_id'; + break; + + case 'associationForeignKey': + $data = Inflector::singularize($this->{$class}->table) . '_id'; + break; + + case 'with': + $data = Inflector::camelize(Inflector::singularize($assoc['joinTable'])); + $dynamicWith = true; + break; + + case 'joinTable': + $tables = [$this->table, $this->{$class}->table]; + sort($tables); + $data = $tables[0] . '_' . $tables[1]; + break; + + case 'className': + $data = $class; + break; + + case 'unique': + $data = true; + break; + } + + $assoc[$key] = $data; + } + + if ($dynamicWith) { + $assoc['dynamicWith'] = true; + } + } + } + + /** + * Returns a list of all events that will fire in the model during it's lifecycle. + * You can override this function to add your own listener callbacks + * + * @return array + */ + public function implementedEvents() + { + return [ + 'Model.beforeFind' => ['callable' => 'beforeFind', 'passParams' => true], + 'Model.afterFind' => ['callable' => 'afterFind', 'passParams' => true], + 'Model.beforeValidate' => ['callable' => 'beforeValidate', 'passParams' => true], + 'Model.afterValidate' => ['callable' => 'afterValidate'], + 'Model.beforeSave' => ['callable' => 'beforeSave', 'passParams' => true], + 'Model.afterSave' => ['callable' => 'afterSave', 'passParams' => true], + 'Model.beforeDelete' => ['callable' => 'beforeDelete', 'passParams' => true], + 'Model.afterDelete' => ['callable' => 'afterDelete'], + ]; + } + + /** + * Handles custom method calls, like findBy for DB models, + * and custom RPC calls for remote data sources. + * + * @param string $method Name of method to call. + * @param array $params Parameters for the method. + * @return mixed Whatever is returned by called method + */ + public function __call($method, $params) + { + $result = $this->Behaviors->dispatchMethod($this, $method, $params); + if ($result !== ['unhandled']) { + return $result; + } + + return $this->getDataSource()->query($method, $params, $this); + } + + /** + * Gets the DataSource to which this model is bound. + * + * @return DataSource A DataSource object + */ + public function getDataSource() + { + if (!$this->_sourceConfigured && $this->useTable !== false) { + $this->_sourceConfigured = true; + $this->setSource($this->useTable); + } + + return ConnectionManager::getDataSource($this->useDbConfig); + } + + /** + * Sets a custom table for your model class. Used by your controller to select a database table. + * + * @param string $tableName Name of the custom table + * @return void + * @throws MissingTableException when database table $tableName is not found on data source + */ + public function setSource($tableName) + { + $this->setDataSource($this->useDbConfig); + $db = ConnectionManager::getDataSource($this->useDbConfig); + + if (method_exists($db, 'listSources')) { + $restore = $db->cacheSources; + $db->cacheSources = ($restore && $this->cacheSources); + $sources = $db->listSources(); + $db->cacheSources = $restore; + + if (is_array($sources) && !in_array(strtolower($this->tablePrefix . $tableName), array_map('strtolower', $sources))) { + throw new MissingTableException([ + 'table' => $this->tablePrefix . $tableName, + 'class' => $this->alias, + 'ds' => $this->useDbConfig, + ]); + } + + if ($sources) { + $this->_schema = null; + } + } + + $this->table = $this->useTable = $tableName; + $this->tableToModel[$this->table] = $this->alias; + } + + /** + * Sets the DataSource to which this model is bound. + * + * @param string $dataSource The name of the DataSource, as defined in app/Config/database.php + * @return void + * @throws MissingConnectionException + */ + public function setDataSource($dataSource = null) + { + $oldConfig = $this->useDbConfig; + + if ($dataSource) { + $this->useDbConfig = $dataSource; + } + + $db = ConnectionManager::getDataSource($this->useDbConfig); + if (!empty($oldConfig) && isset($db->config['prefix'])) { + $oldDb = ConnectionManager::getDataSource($oldConfig); + + if (!isset($this->tablePrefix) || (!isset($oldDb->config['prefix']) || $this->tablePrefix === $oldDb->config['prefix'])) { + $this->tablePrefix = $db->config['prefix']; + } + } else if (isset($db->config['prefix'])) { + $this->tablePrefix = $db->config['prefix']; + } + + $schema = $db->getSchemaName(); + $defaultProperties = get_class_vars(get_class($this)); + if (isset($defaultProperties['schemaName'])) { + $schema = $defaultProperties['schemaName']; + } + $this->schemaName = $schema; + } + + /** + * Handles the lazy loading of model associations by looking in the association arrays for the requested variable + * + * @param string $name variable tested for existence in class + * @return bool true if the variable exists (if is a not loaded model association it will be created), false otherwise + */ + public function __isset($name) + { + $className = false; + + foreach ($this->_associations as $type) { + if (isset($name, $this->{$type}[$name])) { + $className = empty($this->{$type}[$name]['className']) ? $name : $this->{$type}[$name]['className']; + break; + } else if (isset($name, $this->__backAssociation[$type][$name])) { + $className = empty($this->__backAssociation[$type][$name]['className']) ? + $name : $this->__backAssociation[$type][$name]['className']; + break; + } else if ($type === 'hasAndBelongsToMany') { + foreach ($this->{$type} as $k => $relation) { + if (empty($relation['with'])) { + continue; + } + + if (is_array($relation['with'])) { + if (key($relation['with']) === $name) { + $className = $name; + } + } else { + list($plugin, $class) = pluginSplit($relation['with']); + if ($class === $name) { + $className = $relation['with']; + } + } + + if ($className) { + $assocKey = $k; + $dynamic = !empty($relation['dynamicWith']); + break(2); + } + } + } + } + + if (!$className) { + return false; + } + + list($plugin, $className) = pluginSplit($className); + + if (!ClassRegistry::isKeySet($className) && !empty($dynamic)) { + $this->{$className} = new AppModel([ + 'name' => $className, + 'table' => $this->hasAndBelongsToMany[$assocKey]['joinTable'], + 'ds' => $this->useDbConfig + ]); + } else { + $this->_constructLinkedModel($name, $className, $plugin); + } + + if (!empty($assocKey)) { + $this->hasAndBelongsToMany[$assocKey]['joinTable'] = $this->{$name}->table; + if (count($this->{$name}->schema()) <= 2 && $this->{$name}->primaryKey !== false) { + $this->{$name}->primaryKey = $this->hasAndBelongsToMany[$assocKey]['foreignKey']; + } + } + + return true; + } + + /** + * Protected helper method to create associated models of a given class. + * + * @param string $assoc Association name + * @param string $className Class name + * @param string $plugin name of the plugin where $className is located + * examples: public $hasMany = array('Assoc' => array('className' => 'ModelName')); + * usage: $this->Assoc->modelMethods(); + * + * public $hasMany = array('ModelName'); + * usage: $this->ModelName->modelMethods(); + * @return void + */ + protected function _constructLinkedModel($assoc, $className = null, $plugin = null) + { + if (empty($className)) { + $className = $assoc; + } + + if (!isset($this->{$assoc}) || $this->{$assoc}->name !== $className) { + if ($plugin) { + $plugin .= '.'; + } + + $model = ['class' => $plugin . $className, 'alias' => $assoc]; + $this->{$assoc} = ClassRegistry::init($model); + + if ($plugin) { + ClassRegistry::addObject($plugin . $className, $this->{$assoc}); + } + + if ($assoc) { + $this->tableToModel[$this->{$assoc}->table] = $assoc; + } + } + } + + /** + * Returns the value of the requested variable if it can be set by __isset() + * + * @param string $name variable requested for it's value or reference + * @return mixed value of requested variable if it is set + */ + public function __get($name) + { + if ($name === 'displayField') { + return $this->displayField = $this->hasField(['title', 'name', $this->primaryKey]); + } + + if ($name === 'tablePrefix') { + $this->setDataSource(); + if (property_exists($this, 'tablePrefix') && !empty($this->tablePrefix)) { + return $this->tablePrefix; + } + + return $this->tablePrefix = null; + } + + if (isset($this->{$name})) { + return $this->{$name}; + } + } + + /** + * Returns true if the supplied field exists in the model's database table. + * + * @param string|array $name Name of field to look for, or an array of names + * @param bool $checkVirtual checks if the field is declared as virtual + * @return mixed If $name is a string, returns a boolean indicating whether the field exists. + * If $name is an array of field names, returns the first field that exists, + * or false if none exist. + */ + public function hasField($name, $checkVirtual = false) + { + if (is_array($name)) { + foreach ($name as $n) { + if ($this->hasField($n, $checkVirtual)) { + return $n; + } + } + + return false; + } + + if ($checkVirtual && !empty($this->virtualFields) && $this->isVirtualField($name)) { + return true; + } + + if (empty($this->_schema)) { + $this->schema(); + } + + if ($this->_schema) { + return isset($this->_schema[$name]); + } + + return false; + } + + /** + * Returns true if the supplied field is a model Virtual Field + * + * @param string $field Name of field to look for + * @return bool indicating whether the field exists as a model virtual field. + */ + public function isVirtualField($field) + { + if (empty($this->virtualFields) || !is_string($field)) { + return false; + } + + if (isset($this->virtualFields[$field])) { + return true; + } + + if (strpos($field, '.') !== false) { + list($model, $field) = explode('.', $field); + if ($model === $this->alias && isset($this->virtualFields[$field])) { + return true; + } + } + + return false; + } + + /** + * Returns an array of table metadata (column names and types) from the database. + * $field => keys(type, null, default, key, length, extra) + * + * @param bool|string $field Set to true to reload schema, or a string to return a specific field + * @return array|null Array of table metadata + */ + public function schema($field = false) + { + if ($this->useTable !== false && (!is_array($this->_schema) || $field === true)) { + $db = $this->getDataSource(); + $db->cacheSources = ($this->cacheSources && $db->cacheSources); + if (method_exists($db, 'describe')) { + $this->_schema = $db->describe($this); + } + } + + if (!is_string($field)) { + return $this->_schema; + } + + if (isset($this->_schema[$field])) { + return $this->_schema[$field]; + } + + return null; + } + + /** + * Bind model associations on the fly. + * + * If `$reset` is false, association will not be reset + * to the originals defined in the model + * + * Example: Add a new hasOne binding to the Profile model not + * defined in the model source code: + * + * `$this->User->bindModel(array('hasOne' => array('Profile')));` + * + * Bindings that are not made permanent will be reset by the next Model::find() call on this + * model. + * + * @param array $params Set of bindings (indexed by binding type) + * @param bool $reset Set to false to make the binding permanent + * @return bool Success + * @link https://book.cakephp.org/2.0/en/models/associations-linking-models-together.html#creating-and-destroying-associations-on-the-fly + */ + public function bindModel($params, $reset = true) + { + foreach ($params as $assoc => $model) { + if ($reset === true && !isset($this->__backAssociation[$assoc])) { + $this->__backAssociation[$assoc] = $this->{$assoc}; + } + + foreach ($model as $key => $value) { + $assocName = $key; + + if (is_numeric($key)) { + $assocName = $value; + $value = []; + } + + $this->{$assoc}[$assocName] = $value; + + if (property_exists($this, $assocName)) { + unset($this->{$assocName}); + } + + if ($reset === false && isset($this->__backAssociation[$assoc])) { + $this->__backAssociation[$assoc][$assocName] = $value; + } + } + } + + $this->_createLinks(); + return true; + } + + /** + * Turn off associations on the fly. + * + * If $reset is false, association will not be reset + * to the originals defined in the model + * + * Example: Turn off the associated Model Support request, + * to temporarily lighten the User model: + * + * `$this->User->unbindModel(array('hasMany' => array('SupportRequest')));` + * Or alternatively: + * `$this->User->unbindModel(array('hasMany' => 'SupportRequest'));` + * + * Unbound models that are not made permanent will reset with the next call to Model::find() + * + * @param array $params Set of bindings to unbind (indexed by binding type) + * @param bool $reset Set to false to make the unbinding permanent + * @return bool Success + * @link https://book.cakephp.org/2.0/en/models/associations-linking-models-together.html#creating-and-destroying-associations-on-the-fly + */ + public function unbindModel($params, $reset = true) + { + foreach ($params as $assoc => $models) { + if ($reset === true && !isset($this->__backAssociation[$assoc])) { + $this->__backAssociation[$assoc] = $this->{$assoc}; + } + $models = Hash::normalize((array)$models, false); + foreach ($models as $model) { + if ($reset === false && isset($this->__backAssociation[$assoc][$model])) { + unset($this->__backAssociation[$assoc][$model]); + } + + unset($this->{$assoc}[$model]); + } + } + + return true; + } + + /** + * Returns an associative array of field names and column types. + * + * @return array Field types indexed by field name + */ + public function getColumnTypes() + { + $columns = $this->schema(); + if (empty($columns)) { + trigger_error(__d('cake_dev', '(Model::getColumnTypes) Unable to build model field data. If you are using a model without a database table, try implementing schema()'), E_USER_WARNING); + } + + $cols = []; + foreach ($columns as $field => $values) { + $cols[$field] = $values['type']; + } + + return $cols; + } + + /** + * Check that a method is callable on a model. This will check both the model's own methods, its + * inherited methods and methods that could be callable through behaviors. + * + * @param string $method The method to be called. + * @return bool True on method being callable. + */ + public function hasMethod($method) + { + if (method_exists($this, $method)) { + return true; + } + + return $this->Behaviors->hasMethod($method); + } + + /** + * Returns the expression for a model virtual field + * + * @param string $field Name of field to look for + * @return mixed If $field is string expression bound to virtual field $field + * If $field is null, returns an array of all model virtual fields + * or false if none $field exist. + */ + public function getVirtualField($field = null) + { + if (!$field) { + return empty($this->virtualFields) ? false : $this->virtualFields; + } + + if ($this->isVirtualField($field)) { + if (strpos($field, '.') !== false) { + list(, $field) = pluginSplit($field); + } + + return $this->virtualFields[$field]; + } + + return false; + } + + /** + * This function is a convenient wrapper class to create(false) and, as the name suggests, clears the id, data, and validation errors. + * + * @return bool Always true upon success + * @see Model::create() + */ + public function clear() + { + $this->create(false); + return true; + } + + /** + * Initializes the model for writing a new record, loading the default values + * for those fields that are not defined in $data, and clearing previous validation errors. + * Especially helpful for saving data in loops. + * + * @param bool|array $data Optional data array to assign to the model after it is created. If null or false, + * schema data defaults are not merged. + * @param bool $filterKey If true, overwrites any primary key input with an empty value + * @return array The current Model::data; after merging $data and/or defaults from database + * @link https://book.cakephp.org/2.0/en/models/saving-your-data.html#model-create-array-data-array + */ + public function create($data = [], $filterKey = false) + { + $defaults = []; + $this->id = false; + $this->data = []; + $this->validationErrors = []; + + if ($data !== null && $data !== false) { + $schema = (array)$this->schema(); + foreach ($schema as $field => $properties) { + if ($this->primaryKey !== $field && isset($properties['default']) && $properties['default'] !== '') { + $defaults[$field] = $properties['default']; + } + } + + $this->set($defaults); + $this->set($data); + } + + if ($filterKey) { + $this->set($this->primaryKey, false); + } + + return $this->data; + } + + /** + * This function does two things: + * + * 1. it scans the array $one for the primary key, + * and if that's found, it sets the current id to the value of $one[id]. + * For all other keys than 'id' the keys and values of $one are copied to the 'data' property of this object. + * 2. Returns an array with all of $one's keys and values. + * (Alternative indata: two strings, which are mangled to + * a one-item, two-dimensional array using $one for a key and $two as its value.) + * + * @param string|array|SimpleXmlElement|DomNode $one Array or string of data + * @param string|false $two Value string for the alternative indata method + * @return array|null Data with all of $one's keys and values, otherwise null. + * @link https://book.cakephp.org/2.0/en/models/saving-your-data.html + */ + public function set($one, $two = null) + { + if (!$one) { + return null; + } + + if (is_object($one)) { + if ($one instanceof SimpleXMLElement || $one instanceof DOMNode) { + $one = $this->_normalizeXmlData(Xml::toArray($one)); + } else { + $one = Set::reverse($one); + } + } + + if (is_array($one)) { + $data = $one; + if (empty($one[$this->alias])) { + $data = $this->_setAliasData($one); + } + } else { + $data = [$this->alias => [$one => $two]]; + } + + foreach ($data as $modelName => $fieldSet) { + if (!is_array($fieldSet)) { + continue; + } + + if (!isset($this->data[$modelName])) { + $this->data[$modelName] = []; + } + + foreach ($fieldSet as $fieldName => $fieldValue) { + unset($this->validationErrors[$fieldName]); + + if ($modelName === $this->alias && $fieldName === $this->primaryKey) { + $this->id = $fieldValue; + } + + if (is_array($fieldValue) || is_object($fieldValue)) { + $fieldValue = $this->deconstruct($fieldName, $fieldValue); + } + + $this->data[$modelName][$fieldName] = $fieldValue; + } + } + + return $data; + } + + /** + * Normalize `Xml::toArray()` to use in `Model::save()` + * + * @param array $xml XML as array + * @return array + */ + protected function _normalizeXmlData(array $xml) + { + $return = []; + foreach ($xml as $key => $value) { + if (is_array($value)) { + $return[Inflector::camelize($key)] = $this->_normalizeXmlData($value); + } else if ($key[0] === '@') { + $return[substr($key, 1)] = $value; + } else { + $return[$key] = $value; + } + } + + return $return; + } + + /** + * Move values to alias + * + * @param array $data Data. + * @return array + */ + protected function _setAliasData($data) + { + $models = array_keys($this->getAssociated()); + $schema = array_keys((array)$this->schema()); + + foreach ($data as $field => $value) { + if (in_array($field, $schema) || !in_array($field, $models)) { + $data[$this->alias][$field] = $value; + unset($data[$field]); + } + } + + return $data; + } + + /** + * Gets all the models with which this model is associated. + * + * @param string $type Only result associations of this type + * @return array|null Associations + */ + public function getAssociated($type = null) + { + if (!$type) { + $associated = []; + foreach ($this->_associations as $assoc) { + if (!empty($this->{$assoc})) { + $models = array_keys($this->{$assoc}); + foreach ($models as $m) { + $associated[$m] = $assoc; + } + } + } + + return $associated; + } + + if (in_array($type, $this->_associations)) { + if (empty($this->{$type})) { + return []; + } + + return array_keys($this->{$type}); + } + + $assoc = array_merge( + $this->hasOne, + $this->hasMany, + $this->belongsTo, + $this->hasAndBelongsToMany + ); + + if (array_key_exists($type, $assoc)) { + foreach ($this->_associations as $a) { + if (isset($this->{$a}[$type])) { + $assoc[$type]['association'] = $a; + break; + } + } + + return $assoc[$type]; + } + + return null; + } + + /** + * Deconstructs a complex data type (array or object) into a single field value. + * + * @param string $field The name of the field to be deconstructed + * @param array|object $data An array or object to be deconstructed into a field + * @return mixed The resulting data that should be assigned to a field + */ + public function deconstruct($field, $data) + { + if (!is_array($data)) { + return $data; + } + + $type = $this->getColumnType($field); + + if (!in_array($type, ['datetime', 'timestamp', 'date', 'time'])) { + return $data; + } + + $useNewDate = (isset($data['year']) || isset($data['month']) || + isset($data['day']) || isset($data['hour']) || isset($data['minute'])); + + $dateFields = ['Y' => 'year', 'm' => 'month', 'd' => 'day', 'H' => 'hour', 'i' => 'min', 's' => 'sec']; + $timeFields = ['H' => 'hour', 'i' => 'min', 's' => 'sec']; + $date = []; + + if (isset($data['meridian']) && empty($data['meridian'])) { + return null; + } + + if (isset($data['hour']) && + isset($data['meridian']) && + !empty($data['hour']) && + $data['hour'] != 12 && + $data['meridian'] === 'pm' + ) { + $data['hour'] = $data['hour'] + 12; + } + + if (isset($data['hour']) && isset($data['meridian']) && $data['hour'] == 12 && $data['meridian'] === 'am') { + $data['hour'] = '00'; + } + + if ($type === 'time') { + foreach ($timeFields as $key => $val) { + if (!isset($data[$val]) || $data[$val] === '0' || $data[$val] === '00') { + $data[$val] = '00'; + } else if ($data[$val] !== '') { + $data[$val] = sprintf('%02d', $data[$val]); + } + + if (!empty($data[$val])) { + $date[$key] = $data[$val]; + } else { + return null; + } + } + } + + if ($type === 'datetime' || $type === 'timestamp' || $type === 'date') { + foreach ($dateFields as $key => $val) { + if ($val === 'hour' || $val === 'min' || $val === 'sec') { + if (!isset($data[$val]) || $data[$val] === '0' || $data[$val] === '00') { + $data[$val] = '00'; + } else { + $data[$val] = sprintf('%02d', $data[$val]); + } + } + + if (!isset($data[$val]) || isset($data[$val]) && (empty($data[$val]) || substr($data[$val], 0, 1) === '-')) { + return null; + } + + if (isset($data[$val]) && !empty($data[$val])) { + $date[$key] = $data[$val]; + } + } + } + + if ($useNewDate && !empty($date)) { + $format = $this->getDataSource()->columns[$type]['format']; + foreach (['m', 'd', 'H', 'i', 's'] as $index) { + if (isset($date[$index])) { + $date[$index] = sprintf('%02d', $date[$index]); + } + } + + return str_replace(array_keys($date), array_values($date), $format); + } + + return $data; + } + + /** + * Returns the column type of a column in the model. + * + * @param string $column The name of the model column + * @return string Column type + */ + public function getColumnType($column) + { + $cols = $this->schema(); + if (isset($cols[$column]) && isset($cols[$column]['type'])) { + return $cols[$column]['type']; + } + + $db = $this->getDataSource(); + $model = null; + + $startQuote = isset($db->startQuote) ? $db->startQuote : null; + $endQuote = isset($db->endQuote) ? $db->endQuote : null; + $column = str_replace([$startQuote, $endQuote], '', $column); + + if (strpos($column, '.')) { + list($model, $column) = explode('.', $column); + } + + if (isset($model) && $model != $this->alias && isset($this->{$model})) { + return $this->{$model}->getColumnType($column); + } + + if (isset($cols[$column]) && isset($cols[$column]['type'])) { + return $cols[$column]['type']; + } + + return null; + } + + /** + * Returns a list of fields from the database, and sets the current model + * data (Model::$data) with the record found. + * + * @param string|array $fields String of single field name, or an array of field names. + * @param int|string $id The ID of the record to read + * @return array|false Array of database fields, or false if not found + * @link https://book.cakephp.org/2.0/en/models/retrieving-your-data.html#model-read + */ + public function read($fields = null, $id = null) + { + $this->validationErrors = []; + + if ($id) { + $this->id = $id; + } + + $id = $this->id; + + if (is_array($this->id)) { + $id = $this->id[0]; + } + + if ($id !== null && $id !== false) { + $this->data = $this->find('first', [ + 'conditions' => [$this->alias . '.' . $this->primaryKey => $id], + 'fields' => $fields + ]); + + return $this->data; + } + + return false; + } + + /** + * Queries the datasource and returns a result set array. + * + * Used to perform find operations, where the first argument is type of find operation to perform + * (all / first / count / neighbors / list / threaded), + * second parameter options for finding (indexed array, including: 'conditions', 'limit', + * 'recursive', 'page', 'fields', 'offset', 'order', 'callbacks') + * + * Eg: + * ``` + * $model->find('all', array( + * 'conditions' => array('name' => 'Thomas Anderson'), + * 'fields' => array('name', 'email'), + * 'order' => 'field3 DESC', + * 'recursive' => 1, + * 'group' => 'type', + * 'callbacks' => false, + * )); + * ``` + * + * In addition to the standard query keys above, you can provide Datasource, and behavior specific + * keys. For example, when using a SQL based datasource you can use the joins key to specify additional + * joins that should be part of the query. + * + * ``` + * $model->find('all', array( + * 'conditions' => array('name' => 'Thomas Anderson'), + * 'joins' => array( + * array( + * 'alias' => 'Thought', + * 'table' => 'thoughts', + * 'type' => 'LEFT', + * 'conditions' => '`Thought`.`person_id` = `Person`.`id`' + * ) + * ) + * )); + * ``` + * + * ### Disabling callbacks + * + * The `callbacks` key allows you to disable or specify the callbacks that should be run. To + * disable beforeFind & afterFind callbacks set `'callbacks' => false` in your options. You can + * also set the callbacks option to 'before' or 'after' to enable only the specified callback. + * + * ### Adding new find types + * + * Behaviors and find types can also define custom finder keys which are passed into find(). + * See the documentation for custom find types + * (https://book.cakephp.org/2.0/en/models/retrieving-your-data.html#creating-custom-find-types) + * for how to implement custom find types. + * + * Specifying 'fields' for notation 'list': + * + * - If no fields are specified, then 'id' is used for key and 'model->displayField' is used for value. + * - If a single field is specified, 'id' is used for key and specified field is used for value. + * - If three fields are specified, they are used (in order) for key, value and group. + * - Otherwise, first and second fields are used for key and value. + * + * Note: find(list) + database views have issues with MySQL 5.0. Try upgrading to MySQL 5.1 if you + * have issues with database views. + * + * Note: find(count) has its own return values. + * + * @param string $type Type of find operation (all / first / count / neighbors / list / threaded) + * @param array $query Option fields (conditions / fields / joins / limit / offset / order / page / group / callbacks) + * @return array|int|null Array of records, int if the type is count, or Null on failure. + * @link https://book.cakephp.org/2.0/en/models/retrieving-your-data.html + */ + public function find($type = 'first', $query = []) + { + $this->findQueryType = $type; + $this->id = $this->getID(); + + $query = $this->buildQuery($type, $query); + if ($query === null) { + return null; + } + + return $this->_readDataSource($type, $query); + } + + /** + * Returns the current record's ID + * + * @param int $list Index on which the composed ID is located + * @return mixed The ID of the current record, false if no ID + */ + public function getID($list = 0) + { + if (empty($this->id) || (is_array($this->id) && isset($this->id[0]) && empty($this->id[0]))) { + return false; + } + + if (!is_array($this->id)) { + return $this->id; + } + + if (isset($this->id[$list]) && !empty($this->id[$list])) { + return $this->id[$list]; + } + + if (isset($this->id[$list])) { + return false; + } + + return current($this->id); + } + + /** + * Builds the query array that is used by the data source to generate the query to fetch the data. + * + * @param string $type Type of find operation (all / first / count / neighbors / list / threaded) + * @param array $query Option fields (conditions / fields / joins / limit / offset / order / page / group / callbacks) + * @return array|null Query array or null if it could not be build for some reasons + * @triggers Model.beforeFind $this, array($query) + * @see Model::find() + */ + public function buildQuery($type = 'first', $query = []) + { + $query = array_merge( + [ + 'conditions' => null, 'fields' => null, 'joins' => [], 'limit' => null, + 'offset' => null, 'order' => null, 'page' => 1, 'group' => null, 'callbacks' => true, + ], + (array)$query + ); + + if ($this->findMethods[$type] === true) { + $query = $this->{'_find' . ucfirst($type)}('before', $query); + } + + if (!is_numeric($query['page']) || (int)$query['page'] < 1) { + $query['page'] = 1; + } + + if ($query['page'] > 1 && !empty($query['limit'])) { + $query['offset'] = ($query['page'] - 1) * $query['limit']; + } + + if ($query['order'] === null && $this->order !== null) { + $query['order'] = $this->order; + } + + if (is_object($query['order'])) { + $query['order'] = [$query['order']]; + } else { + $query['order'] = (array)$query['order']; + } + + if ($query['callbacks'] === true || $query['callbacks'] === 'before') { + $event = new CakeEvent('Model.beforeFind', $this, [$query]); + list($event->break, $event->breakOn, $event->modParams) = [true, [false, null], 0]; + $this->getEventManager()->dispatch($event); + + if ($event->isStopped()) { + return null; + } + + $query = $event->result === true ? $event->data[0] : $event->result; + } + + return $query; + } + + /** + * Returns the CakeEventManager manager instance that is handling any callbacks. + * You can use this instance to register any new listeners or callbacks to the + * model events, or create your own events and trigger them at will. + * + * @return CakeEventManager + */ + public function getEventManager() + { + if (empty($this->_eventManager)) { + $this->_eventManager = new CakeEventManager(); + $this->_eventManager->attach($this->Behaviors); + $this->_eventManager->attach($this); + } + + return $this->_eventManager; + } + + /** + * Read from the datasource + * + * Model::_readDataSource() is used by all find() calls to read from the data source and can be overloaded to allow + * caching of datasource calls. + * + * ``` + * protected function _readDataSource($type, $query) { + * $cacheName = md5(json_encode($query) . json_encode($this->hasOne) . json_encode($this->belongsTo)); + * $cache = Cache::read($cacheName, 'cache-config-name'); + * if ($cache !== false) { + * return $cache; + * } + * + * $results = parent::_readDataSource($type, $query); + * Cache::write($cacheName, $results, 'cache-config-name'); + * return $results; + * } + * ``` + * + * @param string $type Type of find operation (all / first / count / neighbors / list / threaded) + * @param array $query Option fields (conditions / fields / joins / limit / offset / order / page / group / callbacks) + * @return array + */ + protected function _readDataSource($type, $query) + { + $results = $this->getDataSource()->read($this, $query); + $this->resetAssociations(); + + if ($query['callbacks'] === true || $query['callbacks'] === 'after') { + $results = $this->_filterResults($results); + } + + $this->findQueryType = null; + + if ($this->findMethods[$type] === true) { + return $this->{'_find' . ucfirst($type)}('after', $query, $results); + } + } + + /** + * This resets the association arrays for the model back + * to those originally defined in the model. Normally called at the end + * of each call to Model::find() + * + * @return bool Success + */ + public function resetAssociations() + { + if (!empty($this->__backAssociation)) { + foreach ($this->_associations as $type) { + if (isset($this->__backAssociation[$type])) { + $this->{$type} = $this->__backAssociation[$type]; + } + } + + $this->__backAssociation = []; + } + + foreach ($this->_associations as $type) { + foreach ($this->{$type} as $key => $name) { + if (property_exists($this, $key) && !empty($this->{$key}->__backAssociation)) { + $this->{$key}->resetAssociations(); + } + } + } + + $this->__backAssociation = []; + return true; + } + + /** + * Passes query results through model and behavior afterFind() methods. + * + * @param array $results Results to filter + * @param bool $primary If this is the primary model results (results from model where the find operation was performed) + * @return array Set of filtered results + * @triggers Model.afterFind $this, array($results, $primary) + */ + protected function _filterResults($results, $primary = true) + { + $event = new CakeEvent('Model.afterFind', $this, [$results, $primary]); + $event->modParams = 0; + $this->getEventManager()->dispatch($event); + return $event->result; + } + + /** + * Saves the value of a single field to the database, based on the current + * model ID. + * + * @param string $name Name of the table field + * @param mixed $value Value of the field + * @param bool|array $validate Either a boolean, or an array. + * If a boolean, indicates whether or not to validate before saving. + * If an array, allows control of 'validate', 'callbacks' and 'counterCache' options. + * See Model::save() for details of each options. + * @return bool|array See Model::save() False on failure or an array of model data on success. + * @deprecated 3.0.0 To ease migration to the new major, do not use this method anymore. + * Stateful model usage will be removed. Use the existing save() methods instead. + * @see Model::save() + * @link https://book.cakephp.org/2.0/en/models/saving-your-data.html#model-savefield-string-fieldname-string-fieldvalue-validate-false + */ + public function saveField($name, $value, $validate = false) + { + $id = $this->id; + $this->create(false); + + $options = ['validate' => $validate, 'fieldList' => [$name]]; + if (is_array($validate)) { + $options = $validate + ['validate' => false, 'fieldList' => [$name]]; + } + + return $this->save([$this->alias => [$this->primaryKey => $id, $name => $value]], $options); + } + + /** + * Saves model data (based on white-list, if supplied) to the database. By + * default, validation occurs before save. Passthrough method to _doSave() with + * transaction handling. + * + * @param array $data Data to save. + * @param bool|array $validate Either a boolean, or an array. + * If a boolean, indicates whether or not to validate before saving. + * If an array, can have following keys: + * + * - atomic: If true (default), will attempt to save the record in a single transaction. + * - validate: Set to true/false to enable or disable validation. + * - fieldList: An array of fields you want to allow for saving. + * - callbacks: Set to false to disable callbacks. Using 'before' or 'after' + * will enable only those callbacks. + * - `counterCache`: Boolean to control updating of counter caches (if any) + * + * @param array $fieldList List of fields to allow to be saved + * @return mixed On success Model::$data if its not empty or true, false on failure + * @throws Exception + * @throws PDOException + * @triggers Model.beforeSave $this, array($options) + * @triggers Model.afterSave $this, array($created, $options) + * @link https://book.cakephp.org/2.0/en/models/saving-your-data.html + */ + public function save($data = null, $validate = true, $fieldList = []) + { + $defaults = [ + 'validate' => true, 'fieldList' => [], + 'callbacks' => true, 'counterCache' => true, + 'atomic' => true + ]; + + if (!is_array($validate)) { + $options = compact('validate', 'fieldList') + $defaults; + } else { + $options = $validate + $defaults; + } + + if (!$options['atomic']) { + return $this->_doSave($data, $options); + } + + $db = $this->getDataSource(); + $transactionBegun = $db->begin(); + try { + $success = $this->_doSave($data, $options); + if ($transactionBegun) { + if ($success) { + $db->commit(); + } else { + $db->rollback(); + } + } + return $success; + } catch (Exception $e) { + if ($transactionBegun) { + $db->rollback(); + } + throw $e; + } + } + + /** + * Saves model data (based on white-list, if supplied) to the database. By + * default, validation occurs before save. + * + * @param array $data Data to save. + * @param array $options can have following keys: + * + * - validate: Set to true/false to enable or disable validation. + * - fieldList: An array of fields you want to allow for saving. + * - callbacks: Set to false to disable callbacks. Using 'before' or 'after' + * will enable only those callbacks. + * - `counterCache`: Boolean to control updating of counter caches (if any) + * + * @return mixed On success Model::$data if its not empty or true, false on failure + * @throws PDOException + * @link https://book.cakephp.org/2.0/en/models/saving-your-data.html + */ + protected function _doSave($data = null, $options = []) + { + $_whitelist = $this->whitelist; + $fields = []; + + if (!empty($options['fieldList'])) { + if (!empty($options['fieldList'][$this->alias]) && is_array($options['fieldList'][$this->alias])) { + $this->whitelist = $options['fieldList'][$this->alias]; + } else if (Hash::dimensions($options['fieldList']) < 2) { + $this->whitelist = $options['fieldList']; + } + } else if ($options['fieldList'] === null) { + $this->whitelist = []; + } + + $this->set($data); + + if (empty($this->data) && !$this->hasField(['created', 'updated', 'modified'])) { + $this->whitelist = $_whitelist; + return false; + } + + foreach (['created', 'updated', 'modified'] as $field) { + $keyPresentAndEmpty = ( + isset($this->data[$this->alias]) && + array_key_exists($field, $this->data[$this->alias]) && + $this->data[$this->alias][$field] === null + ); + + if ($keyPresentAndEmpty) { + unset($this->data[$this->alias][$field]); + } + } + + $exists = $this->exists($this->getID()); + $dateFields = ['modified', 'updated']; + + if (!$exists) { + $dateFields[] = 'created'; + } + + if (isset($this->data[$this->alias])) { + $fields = array_keys($this->data[$this->alias]); + } + + if ($options['validate'] && !$this->validates($options)) { + $this->whitelist = $_whitelist; + return false; + } + + $db = $this->getDataSource(); + $now = time(); + + foreach ($dateFields as $updateCol) { + $fieldHasValue = in_array($updateCol, $fields); + $fieldInWhitelist = ( + count($this->whitelist) === 0 || + in_array($updateCol, $this->whitelist) + ); + if (($fieldHasValue && $fieldInWhitelist) || !$this->hasField($updateCol)) { + continue; + } + + $default = ['formatter' => 'date']; + $colType = array_merge($default, $db->columns[$this->getColumnType($updateCol)]); + + $time = $now; + if (array_key_exists('format', $colType)) { + $time = call_user_func($colType['formatter'], $colType['format']); + } + + if (!empty($this->whitelist)) { + $this->whitelist[] = $updateCol; + } + $this->set($updateCol, $time); + } + + if ($options['callbacks'] === true || $options['callbacks'] === 'before') { + $event = new CakeEvent('Model.beforeSave', $this, [$options]); + list($event->break, $event->breakOn) = [true, [false, null]]; + $this->getEventManager()->dispatch($event); + if (!$event->result) { + $this->whitelist = $_whitelist; + return false; + } + } + + if (empty($this->data[$this->alias][$this->primaryKey])) { + unset($this->data[$this->alias][$this->primaryKey]); + } + $joined = $fields = $values = []; + + foreach ($this->data as $n => $v) { + if (isset($this->hasAndBelongsToMany[$n])) { + if (isset($v[$n])) { + $v = $v[$n]; + } + $joined[$n] = $v; + } else if ($n === $this->alias) { + foreach (['created', 'updated', 'modified'] as $field) { + if (array_key_exists($field, $v) && empty($v[$field])) { + unset($v[$field]); + } + } + + foreach ($v as $x => $y) { + if ($this->hasField($x) && (empty($this->whitelist) || in_array($x, $this->whitelist))) { + list($fields[], $values[]) = [$x, $y]; + } + } + } + } + + if (empty($fields) && empty($joined)) { + $this->whitelist = $_whitelist; + return false; + } + + $count = count($fields); + + if (!$exists && $count > 0) { + $this->id = false; + } + + $success = true; + $created = false; + + if ($count > 0) { + $cache = $this->_prepareUpdateFields(array_combine($fields, $values)); + + if (!empty($this->id)) { + $this->__safeUpdateMode = true; + try { + $success = (bool)$db->update($this, $fields, $values); + } catch (Exception $e) { + $this->__safeUpdateMode = false; + throw $e; + } + $this->__safeUpdateMode = false; + } else { + if (empty($this->data[$this->alias][$this->primaryKey]) && $this->_isUUIDField($this->primaryKey)) { + if (array_key_exists($this->primaryKey, $this->data[$this->alias])) { + $j = array_search($this->primaryKey, $fields); + $values[$j] = CakeText::uuid(); + } else { + list($fields[], $values[]) = [$this->primaryKey, CakeText::uuid()]; + } + } + + if (!$db->create($this, $fields, $values)) { + $success = false; + } else { + $created = true; + } + } + + if ($success && $options['counterCache'] && !empty($this->belongsTo)) { + $this->updateCounterCache($cache, $created); + } + } + + if ($success && !empty($joined)) { + $this->_saveMulti($joined, $this->id, $db); + } + + if (!$success) { + $this->whitelist = $_whitelist; + return $success; + } + + if ($count > 0) { + if ($created) { + $this->data[$this->alias][$this->primaryKey] = $this->id; + } + + if ($options['callbacks'] === true || $options['callbacks'] === 'after') { + $event = new CakeEvent('Model.afterSave', $this, [$created, $options]); + $this->getEventManager()->dispatch($event); + } + } + + if (!empty($this->data)) { + $success = $this->data; + } + + $this->_clearCache(); + $this->validationErrors = []; + $this->whitelist = $_whitelist; + $this->data = false; + + return $success; + } + + /** + * Returns true if a record with particular ID exists. + * + * If $id is not passed it calls `Model::getID()` to obtain the current record ID, + * and then performs a `Model::find('count')` on the currently configured datasource + * to ascertain the existence of the record in persistent storage. + * + * @param int|string $id ID of record to check for existence + * @return bool True if such a record exists + */ + public function exists($id = null) + { + if ($id === null) { + $id = $this->getID(); + } + + if ($id === false) { + return false; + } + + if ($this->useTable === false) { + return false; + } + + return (bool)$this->find('count', [ + 'conditions' => [ + $this->alias . '.' . $this->primaryKey => $id + ], + 'recursive' => -1, + 'callbacks' => false + ]); + } + + /** + * Returns true if all fields pass validation. Will validate hasAndBelongsToMany associations + * that use the 'with' key as well. Since _saveMulti is incapable of exiting a save operation. + * + * Will validate the currently set data. Use Model::set() or Model::create() to set the active data. + * + * @param array $options An optional array of custom options to be made available in the beforeValidate callback + * @return bool True if there are no errors + */ + public function validates($options = []) + { + return $this->validator()->validates($options); + } + + /** + * Returns an instance of a model validator for this class + * + * @param ModelValidator $instance Model validator instance. + * If null a new ModelValidator instance will be made using current model object + * @return ModelValidator + */ + public function validator(ModelValidator $instance = null) + { + if ($instance) { + $this->_validator = $instance; + } else if (!$this->_validator) { + $this->_validator = new ModelValidator($this); + } + + return $this->_validator; + } + + /** + * Helper method for `Model::updateCounterCache()`. Checks the fields to be updated for + * + * @param array $data The fields of the record that will be updated + * @return array Returns updated foreign key values, along with an 'old' key containing the old + * values, or empty if no foreign keys are updated. + */ + protected function _prepareUpdateFields($data) + { + $foreignKeys = []; + foreach ($this->belongsTo as $assoc => $info) { + if (isset($info['counterCache']) && $info['counterCache']) { + $foreignKeys[$assoc] = $info['foreignKey']; + } + } + + $included = array_intersect($foreignKeys, array_keys($data)); + + if (empty($included) || empty($this->id)) { + return []; + } + + $old = $this->find('first', [ + 'conditions' => [$this->alias . '.' . $this->primaryKey => $this->id], + 'fields' => array_values($included), + 'recursive' => -1 + ]); + + return array_merge($data, ['old' => $old[$this->alias]]); + } + + /** + * Check if the passed in field is a UUID field + * + * @param string $field the field to check + * @return bool + */ + protected function _isUUIDField($field) + { + $field = $this->schema($field); + return $field !== null && $field['length'] == 36 && in_array($field['type'], ['string', 'binary', 'uuid']); + } + + /** + * Updates the counter cache of belongsTo associations after a save or delete operation + * + * @param array $keys Optional foreign key data, defaults to the information $this->data + * @param bool $created True if a new record was created, otherwise only associations with + * 'counterScope' defined get updated + * @return void + */ + public function updateCounterCache($keys = [], $created = false) + { + if (empty($keys) && isset($this->data[$this->alias])) { + $keys = $this->data[$this->alias]; + } + $keys['old'] = isset($keys['old']) ? $keys['old'] : []; + + foreach ($this->belongsTo as $parent => $assoc) { + if (empty($assoc['counterCache'])) { + continue; + } + + $Model = $this->{$parent}; + + if (!is_array($assoc['counterCache'])) { + if (isset($assoc['counterScope'])) { + $assoc['counterCache'] = [$assoc['counterCache'] => $assoc['counterScope']]; + } else { + $assoc['counterCache'] = [$assoc['counterCache'] => []]; + } + } + + $foreignKey = $assoc['foreignKey']; + $fkQuoted = $this->escapeField($assoc['foreignKey']); + + foreach ($assoc['counterCache'] as $field => $conditions) { + if (!is_string($field)) { + $field = Inflector::underscore($this->alias) . '_count'; + } + + if (!$Model->hasField($field)) { + continue; + } + + if ($conditions === true) { + $conditions = []; + } else { + $conditions = (array)$conditions; + } + + if (!array_key_exists($foreignKey, $keys)) { + $keys[$foreignKey] = $this->field($foreignKey); + } + + $recursive = (empty($conditions) ? -1 : 0); + + if (isset($keys['old'][$foreignKey]) && $keys['old'][$foreignKey] != $keys[$foreignKey]) { + $conditions[$fkQuoted] = $keys['old'][$foreignKey]; + $count = (int)$this->find('count', compact('conditions', 'recursive')); + + $Model->updateAll( + [$field => $count], + [$Model->escapeField() => $keys['old'][$foreignKey]] + ); + } + + $conditions[$fkQuoted] = $keys[$foreignKey]; + + if ($recursive === 0) { + $conditions = array_merge($conditions, (array)$conditions); + } + + $count = (int)$this->find('count', compact('conditions', 'recursive')); + + $Model->updateAll( + [$field => $count], + [$Model->escapeField() => $keys[$foreignKey]] + ); + } + } + } + + /** + * Escapes the field name and prepends the model name. Escaping is done according to the + * current database driver's rules. + * + * @param string $field Field to escape (e.g: id) + * @param string $alias Alias for the model (e.g: Post) + * @return string The name of the escaped field for this Model (i.e. id becomes `Post`.`id`). + */ + public function escapeField($field = null, $alias = null) + { + if (empty($alias)) { + $alias = $this->alias; + } + + if (empty($field)) { + $field = $this->primaryKey; + } + + $db = $this->getDataSource(); + if (strpos($field, $db->name($alias) . '.') === 0) { + return $field; + } + + return $db->name($alias . '.' . $field); + } + + /** + * Returns the content of a single field given the supplied conditions, + * of the first record in the supplied order. + * + * @param string $name The name of the field to get. + * @param array $conditions SQL conditions (defaults to NULL). + * @param string|array $order SQL ORDER BY fragment. + * @return string|false Field content, or false if not found. + * @link https://book.cakephp.org/2.0/en/models/retrieving-your-data.html#model-field + */ + public function field($name, $conditions = null, $order = null) + { + if ($conditions === null && !in_array($this->id, [false, null], true)) { + $conditions = [$this->alias . '.' . $this->primaryKey => $this->id]; + } + + $recursive = $this->recursive; + if ($this->recursive >= 1) { + $recursive = -1; + } + + $fields = $name; + $data = $this->find('first', compact('conditions', 'fields', 'order', 'recursive')); + if (!$data) { + return false; + } + + if (strpos($name, '.') === false) { + if (isset($data[$this->alias][$name])) { + return $data[$this->alias][$name]; + } + } else { + $name = explode('.', $name); + if (isset($data[$name[0]][$name[1]])) { + return $data[$name[0]][$name[1]]; + } + } + + if (isset($data[0]) && count($data[0]) > 0) { + return array_shift($data[0]); + } + } + + /** + * Saves model hasAndBelongsToMany data to the database. + * + * @param array $joined Data to save + * @param int|string $id ID of record in this model + * @param DataSource $db Datasource instance. + * @return void + */ + protected function _saveMulti($joined, $id, $db) + { + foreach ($joined as $assoc => $data) { + if (!isset($this->hasAndBelongsToMany[$assoc])) { + continue; + } + + $habtm = $this->hasAndBelongsToMany[$assoc]; + + list($join) = $this->joinModel($habtm['with']); + + $Model = $this->{$join}; + + if (!empty($habtm['with'])) { + $withModel = is_array($habtm['with']) ? key($habtm['with']) : $habtm['with']; + list(, $withModel) = pluginSplit($withModel); + $dbMulti = $this->{$withModel}->getDataSource(); + } else { + $dbMulti = $db; + } + + $isUUID = !empty($Model->primaryKey) && $Model->_isUUIDField($Model->primaryKey); + + $newData = $newValues = $newJoins = []; + $primaryAdded = false; + + $fields = [ + $dbMulti->name($habtm['foreignKey']), + $dbMulti->name($habtm['associationForeignKey']) + ]; + + $idField = $db->name($Model->primaryKey); + if ($isUUID && !in_array($idField, $fields)) { + $fields[] = $idField; + $primaryAdded = true; + } + + foreach ((array)$data as $row) { + if ((is_string($row) && (strlen($row) === 36 || strlen($row) === 16)) || is_numeric($row)) { + $newJoins[] = $row; + $values = [$id, $row]; + + if ($isUUID && $primaryAdded) { + $values[] = CakeText::uuid(); + } + + $newValues[$row] = $values; + unset($values); + } else if (isset($row[$habtm['associationForeignKey']])) { + if (!empty($row[$Model->primaryKey])) { + $newJoins[] = $row[$habtm['associationForeignKey']]; + } + + $newData[] = $row; + } else if (isset($row[$join]) && isset($row[$join][$habtm['associationForeignKey']])) { + if (!empty($row[$join][$Model->primaryKey])) { + $newJoins[] = $row[$join][$habtm['associationForeignKey']]; + } + + $newData[] = $row[$join]; + } + } + + $keepExisting = $habtm['unique'] === 'keepExisting'; + if ($habtm['unique']) { + $conditions = [ + $join . '.' . $habtm['foreignKey'] => $id + ]; + + if (!empty($habtm['conditions'])) { + $conditions = array_merge($conditions, (array)$habtm['conditions']); + } + + $associationForeignKey = $Model->alias . '.' . $habtm['associationForeignKey']; + $links = $Model->find('all', [ + 'conditions' => $conditions, + 'recursive' => empty($habtm['conditions']) ? -1 : 0, + 'fields' => $associationForeignKey, + ]); + + $oldLinks = Hash::extract($links, "{n}.{$associationForeignKey}"); + if (!empty($oldLinks)) { + if ($keepExisting && !empty($newJoins)) { + $conditions[$associationForeignKey] = array_diff($oldLinks, $newJoins); + } else { + $conditions[$associationForeignKey] = $oldLinks; + } + + $dbMulti->delete($Model, $conditions); + } + } + + if (!empty($newData)) { + foreach ($newData as $data) { + $data[$habtm['foreignKey']] = $id; + if (empty($data[$Model->primaryKey])) { + $Model->create(); + } + + $Model->save($data, ['atomic' => false]); + } + } + + if (!empty($newValues)) { + if ($keepExisting && !empty($links)) { + foreach ($links as $link) { + $oldJoin = $link[$join][$habtm['associationForeignKey']]; + if (!in_array($oldJoin, $newJoins)) { + $conditions[$associationForeignKey] = $oldJoin; + $db->delete($Model, $conditions); + } else { + unset($newValues[$oldJoin]); + } + } + + $newValues = array_values($newValues); + } + + if (!empty($newValues)) { + $dbMulti->insertMulti($Model, $fields, $newValues); + } + } + } + } + + /** + * Gets the name and fields to be used by a join model. This allows specifying join fields + * in the association definition. + * + * @param string|array $assoc The model to be joined + * @param array $keys Any join keys which must be merged with the keys queried + * @return array + */ + public function joinModel($assoc, $keys = []) + { + if (is_string($assoc)) { + list(, $assoc) = pluginSplit($assoc); + return [$assoc, array_keys($this->{$assoc}->schema())]; + } + + if (is_array($assoc)) { + $with = key($assoc); + return [$with, array_unique(array_merge($assoc[$with], $keys))]; + } + + trigger_error( + __d('cake_dev', 'Invalid join model settings in %s. The association parameter has the wrong type, expecting a string or array, but was passed type: %s', $this->alias, gettype($assoc)), + E_USER_WARNING + ); + } + + /** + * Clears cache for this model. + * + * @param string $type If null this deletes cached views if Cache.check is true + * Will be used to allow deleting query cache also + * @return mixed True on delete, null otherwise + */ + protected function _clearCache($type = null) + { + if ($type !== null || Configure::read('Cache.check') !== true) { + return; + } + $pluralized = Inflector::pluralize($this->alias); + $assoc = [ + strtolower($pluralized), + Inflector::underscore($pluralized) + ]; + foreach ($this->_associations as $association) { + foreach ($this->{$association} as $className) { + $pluralizedAssociation = Inflector::pluralize($className['className']); + if (!in_array(strtolower($pluralizedAssociation), $assoc)) { + $assoc = array_merge($assoc, [ + strtolower($pluralizedAssociation), + Inflector::underscore($pluralizedAssociation) + ]); + } + } + } + clearCache(array_unique($assoc)); + return true; + } + + /** + * Backwards compatible passthrough method for: + * saveMany(), validateMany(), saveAssociated() and validateAssociated() + * + * Saves multiple individual records for a single model; Also works with a single record, as well as + * all its associated records. + * + * #### Options + * + * - `validate`: Set to false to disable validation, true to validate each record before saving, + * 'first' to validate *all* records before any are saved (default), + * or 'only' to only validate the records, but not save them. + * - `atomic`: If true (default), will attempt to save all records in a single transaction. + * Should be set to false if database/table does not support transactions. + * - `fieldList`: Equivalent to the $fieldList parameter in Model::save(). + * It should be an associate array with model name as key and array of fields as value. Eg. + * ``` + * array( + * 'SomeModel' => array('field'), + * 'AssociatedModel' => array('field', 'otherfield') + * ) + * ``` + * - `deep`: See saveMany/saveAssociated + * - `callbacks`: See Model::save() + * - `counterCache`: See Model::save() + * + * @param array $data Record data to save. This can be either a numerically-indexed array (for saving multiple + * records of the same type), or an array indexed by association name. + * @param array $options Options to use when saving record data, See $options above. + * @return mixed If atomic: True on success, or false on failure. + * Otherwise: array similar to the $data array passed, but values are set to true/false + * depending on whether each record saved successfully. + * @link https://book.cakephp.org/2.0/en/models/saving-your-data.html#model-saveassociated-array-data-null-array-options-array + * @link https://book.cakephp.org/2.0/en/models/saving-your-data.html#model-saveall-array-data-null-array-options-array + */ + public function saveAll($data = [], $options = []) + { + $options += ['validate' => 'first']; + if (Hash::numeric(array_keys($data))) { + if ($options['validate'] === 'only') { + return $this->validateMany($data, $options); + } + + return $this->saveMany($data, $options); + } + + if ($options['validate'] === 'only') { + return $this->validateAssociated($data, $options); + } + + return $this->saveAssociated($data, $options); + } + + /** + * Validates multiple individual records for a single model + * + * #### Options + * + * - `atomic`: If true (default), returns boolean. If false returns array. + * - `fieldList`: Equivalent to the $fieldList parameter in Model::save() + * - `deep`: If set to true, all associated data will be validated as well. + * + * Warning: This method could potentially change the passed argument `$data`, + * If you do not want this to happen, make a copy of `$data` before passing it + * to this method + * + * @param array &$data Record data to validate. This should be a numerically-indexed array + * @param array $options Options to use when validating record data (see above), See also $options of validates(). + * @return bool|array If atomic: True on success, or false on failure. + * Otherwise: array similar to the $data array passed, but values are set to true/false + * depending on whether each record validated successfully. + */ + public function validateMany(&$data, $options = []) + { + return $this->validator()->validateMany($data, $options); + } + + /** + * Saves multiple individual records for a single model + * + * #### Options + * + * - `validate`: Set to false to disable validation, true to validate each record before saving, + * 'first' to validate *all* records before any are saved (default), + * - `atomic`: If true (default), will attempt to save all records in a single transaction. + * Should be set to false if database/table does not support transactions. + * - `fieldList`: Equivalent to the $fieldList parameter in Model::save() + * - `deep`: If set to true, all associated data will be saved as well. + * - `callbacks`: See Model::save() + * - `counterCache`: See Model::save() + * + * @param array $data Record data to save. This should be a numerically-indexed array + * @param array $options Options to use when saving record data, See $options above. + * @return mixed If atomic: True on success, or false on failure. + * Otherwise: array similar to the $data array passed, but values are set to true/false + * depending on whether each record saved successfully. + * @throws PDOException + * @link https://book.cakephp.org/2.0/en/models/saving-your-data.html#model-savemany-array-data-null-array-options-array + */ + public function saveMany($data = null, $options = []) + { + if (empty($data)) { + $data = $this->data; + } + + $options += ['validate' => 'first', 'atomic' => true, 'deep' => false]; + $this->validationErrors = $validationErrors = []; + + if (empty($data) && $options['validate'] !== false) { + $result = $this->save($data, $options); + if (!$options['atomic']) { + return [!empty($result)]; + } + + return !empty($result); + } + + if ($options['validate'] === 'first') { + $validates = $this->validateMany($data, $options); + if ((!$validates && $options['atomic']) || (!$options['atomic'] && in_array(false, $validates, true))) { + return $validates; + } + $options['validate'] = false; + } + + $transactionBegun = false; + if ($options['atomic']) { + $db = $this->getDataSource(); + $transactionBegun = $db->begin(); + } + + try { + $return = []; + foreach ($data as $key => $record) { + $validates = $this->create(null) !== null; + $saved = false; + if ($validates) { + if ($options['deep']) { + $saved = $this->saveAssociated($record, ['atomic' => false] + $options); + } else { + $saved = (bool)$this->save($record, ['atomic' => false] + $options); + } + } + + $validates = ($validates && ($saved === true || (is_array($saved) && !in_array(false, Hash::flatten($saved), true)))); + if (!$validates) { + $validationErrors[$key] = $this->validationErrors; + } + + if (!$options['atomic']) { + $return[$key] = $validates; + } else if (!$validates) { + break; + } + } + + $this->validationErrors = $validationErrors; + + if (!$options['atomic']) { + return $return; + } + + if ($validates) { + if ($transactionBegun) { + return $db->commit() !== false; + } + return true; + } + + if ($transactionBegun) { + $db->rollback(); + } + return false; + } catch (Exception $e) { + if ($transactionBegun) { + $db->rollback(); + } + throw $e; + } + } + + /** + * Saves a single record, as well as all its directly associated records. + * + * #### Options + * + * - `validate`: Set to `false` to disable validation, `true` to validate each record before saving, + * 'first' to validate *all* records before any are saved(default), + * - `atomic`: If true (default), will attempt to save all records in a single transaction. + * Should be set to false if database/table does not support transactions. + * - `fieldList`: Equivalent to the $fieldList parameter in Model::save(). + * It should be an associate array with model name as key and array of fields as value. Eg. + * ``` + * array( + * 'SomeModel' => array('field'), + * 'AssociatedModel' => array('field', 'otherfield') + * ) + * ``` + * - `deep`: If set to true, not only directly associated data is saved, but deeper nested associated data as well. + * - `callbacks`: See Model::save() + * - `counterCache`: See Model::save() + * + * @param array $data Record data to save. This should be an array indexed by association name. + * @param array $options Options to use when saving record data, See $options above. + * @return mixed If atomic: True on success, or false on failure. + * Otherwise: array similar to the $data array passed, but values are set to true/false + * depending on whether each record saved successfully. + * @throws PDOException + * @link https://book.cakephp.org/2.0/en/models/saving-your-data.html#model-saveassociated-array-data-null-array-options-array + */ + public function saveAssociated($data = null, $options = []) + { + if (empty($data)) { + $data = $this->data; + } + + $options += ['validate' => 'first', 'atomic' => true, 'deep' => false]; + $this->validationErrors = $validationErrors = []; + + if (empty($data) && $options['validate'] !== false) { + $result = $this->save($data, $options); + if (!$options['atomic']) { + return [!empty($result)]; + } + + return !empty($result); + } + + if ($options['validate'] === 'first') { + $validates = $this->validateAssociated($data, $options); + if ((!$validates && $options['atomic']) || (!$options['atomic'] && in_array(false, Hash::flatten($validates), true))) { + return $validates; + } + + $options['validate'] = false; + } + + $transactionBegun = false; + if ($options['atomic']) { + $db = $this->getDataSource(); + $transactionBegun = $db->begin(); + } + + try { + $associations = $this->getAssociated(); + $return = []; + $validates = true; + foreach ($data as $association => $values) { + $isEmpty = empty($values) || (isset($values[$association]) && empty($values[$association])); + if ($isEmpty || !isset($associations[$association]) || $associations[$association] !== 'belongsTo') { + continue; + } + + $Model = $this->{$association}; + + $validates = $Model->create(null) !== null; + $saved = false; + if ($validates) { + if ($options['deep']) { + $saved = $Model->saveAssociated($values, ['atomic' => false] + $options); + } else { + $saved = (bool)$Model->save($values, ['atomic' => false] + $options); + } + $validates = ($saved === true || (is_array($saved) && !in_array(false, Hash::flatten($saved), true))); + } + + if ($validates) { + $key = $this->belongsTo[$association]['foreignKey']; + if (isset($data[$this->alias])) { + $data[$this->alias][$key] = $Model->id; + } else { + $data = array_merge([$key => $Model->id], $data, [$key => $Model->id]); + } + $options = $this->_addToWhiteList($key, $options); + } else { + $validationErrors[$association] = $Model->validationErrors; + } + + $return[$association] = $validates; + } + + if ($validates && !($this->create(null) !== null && $this->save($data, ['atomic' => false] + $options))) { + $validationErrors[$this->alias] = $this->validationErrors; + $validates = false; + } + $return[$this->alias] = $validates; + + foreach ($data as $association => $values) { + if (!$validates) { + break; + } + + $isEmpty = empty($values) || (isset($values[$association]) && empty($values[$association])); + if ($isEmpty || !isset($associations[$association])) { + continue; + } + + $Model = $this->{$association}; + + $type = $associations[$association]; + $key = $this->{$type}[$association]['foreignKey']; + switch ($type) { + case 'hasOne': + if (isset($values[$association])) { + $values[$association][$key] = $this->id; + } else { + $values = array_merge([$key => $this->id], $values, [$key => $this->id]); + } + + $validates = $Model->create(null) !== null; + $saved = false; + + if ($validates) { + $options = $Model->_addToWhiteList($key, $options); + if ($options['deep']) { + $saved = $Model->saveAssociated($values, ['atomic' => false] + $options); + } else { + $saved = (bool)$Model->save($values, $options); + } + } + + $validates = ($validates && ($saved === true || (is_array($saved) && !in_array(false, Hash::flatten($saved), true)))); + if (!$validates) { + $validationErrors[$association] = $Model->validationErrors; + } + + $return[$association] = $validates; + break; + case 'hasMany': + foreach ($values as $i => $value) { + if (isset($values[$i][$association])) { + $values[$i][$association][$key] = $this->id; + } else { + $values[$i] = array_merge([$key => $this->id], $value, [$key => $this->id]); + } + } + + $options = $Model->_addToWhiteList($key, $options); + $_return = $Model->saveMany($values, ['atomic' => false] + $options); + if (in_array(false, $_return, true)) { + $validationErrors[$association] = $Model->validationErrors; + $validates = false; + } + + $return[$association] = $_return; + break; + } + } + $this->validationErrors = $validationErrors; + + if (isset($validationErrors[$this->alias])) { + $this->validationErrors = $validationErrors[$this->alias]; + unset($validationErrors[$this->alias]); + $this->validationErrors = array_merge($this->validationErrors, $validationErrors); + } + + if (!$options['atomic']) { + return $return; + } + if ($validates) { + if ($transactionBegun) { + return $db->commit() !== false; + } + + return true; + } + + if ($transactionBegun) { + $db->rollback(); + } + return false; + } catch (Exception $e) { + if ($transactionBegun) { + $db->rollback(); + } + throw $e; + } + } + + /** + * Validates a single record, as well as all its directly associated records. + * + * #### Options + * + * - `atomic`: If true (default), returns boolean. If false returns array. + * - `fieldList`: Equivalent to the $fieldList parameter in Model::save() + * - `deep`: If set to true, not only directly associated data , but deeper nested associated data is validated as well. + * + * Warning: This method could potentially change the passed argument `$data`, + * If you do not want this to happen, make a copy of `$data` before passing it + * to this method + * + * @param array &$data Record data to validate. This should be an array indexed by association name. + * @param array $options Options to use when validating record data (see above), See also $options of validates(). + * @return array|bool If atomic: True on success, or false on failure. + * Otherwise: array similar to the $data array passed, but values are set to true/false + * depending on whether each record validated successfully. + */ + public function validateAssociated(&$data, $options = []) + { + return $this->validator()->validateAssociated($data, $options); + } + + /** + * Helper method for saveAll() and friends, to add foreign key to fieldlist + * + * @param string $key fieldname to be added to list + * @param array $options Options list + * @return array options + */ + protected function _addToWhiteList($key, $options) + { + if (empty($options['fieldList']) && $this->whitelist && !in_array($key, $this->whitelist)) { + $options['fieldList'][$this->alias] = $this->whitelist; + $options['fieldList'][$this->alias][] = $key; + return $options; + } + + if (!empty($options['fieldList'][$this->alias]) && is_array($options['fieldList'][$this->alias])) { + $options['fieldList'][$this->alias][] = $key; + return $options; + } + + if (!empty($options['fieldList']) && is_array($options['fieldList']) && Hash::dimensions($options['fieldList']) < 2) { + $options['fieldList'][] = $key; + } + + return $options; + } + + /** + * Updates multiple model records based on a set of conditions. + * + * @param array $fields Set of fields and values, indexed by fields. + * Fields are treated as SQL snippets, to insert literal values manually escape your data. + * @param mixed $conditions Conditions to match, true for all records + * @return bool True on success, false on failure + * @link https://book.cakephp.org/2.0/en/models/saving-your-data.html#model-updateall-array-fields-mixed-conditions + */ + public function updateAll($fields, $conditions = true) + { + return $this->getDataSource()->update($this, $fields, null, $conditions); + } + + /** + * Deletes multiple model records based on a set of conditions. + * + * @param mixed $conditions Conditions to match + * @param bool $cascade Set to true to delete records that depend on this record + * @param bool $callbacks Run callbacks + * @return bool True on success, false on failure + * @link https://book.cakephp.org/2.0/en/models/deleting-data.html#deleteall + */ + public function deleteAll($conditions, $cascade = true, $callbacks = false) + { + if (empty($conditions)) { + return false; + } + + $db = $this->getDataSource(); + + if (!$cascade && !$callbacks) { + return $db->delete($this, $conditions); + } + + $ids = $this->find('all', array_merge([ + 'fields' => "{$this->alias}.{$this->primaryKey}", + 'order' => false, + 'group' => "{$this->alias}.{$this->primaryKey}", + 'recursive' => 0], compact('conditions')) + ); + + if ($ids === false || $ids === null) { + return false; + } + + $ids = Hash::extract($ids, "{n}.{$this->alias}.{$this->primaryKey}"); + if (empty($ids)) { + return true; + } + + if ($callbacks) { + $_id = $this->id; + $result = true; + foreach ($ids as $id) { + $result = $result && $this->delete($id, $cascade); + } + + $this->id = $_id; + return $result; + } + + foreach ($ids as $id) { + $this->_deleteLinks($id); + if ($cascade) { + $this->_deleteDependent($id, $cascade); + } + } + + return $db->delete($this, [$this->alias . '.' . $this->primaryKey => $ids]); + } + + /** + * Removes record for given ID. If no ID is given, the current ID is used. Returns true on success. + * + * @param int|string $id ID of record to delete + * @param bool $cascade Set to true to delete records that depend on this record + * @return bool True on success + * @triggers Model.beforeDelete $this, array($cascade) + * @triggers Model.afterDelete $this + * @link https://book.cakephp.org/2.0/en/models/deleting-data.html + */ + public function delete($id = null, $cascade = true) + { + if (!empty($id)) { + $this->id = $id; + } + + $id = $this->id; + + $event = new CakeEvent('Model.beforeDelete', $this, [$cascade]); + list($event->break, $event->breakOn) = [true, [false, null]]; + $this->getEventManager()->dispatch($event); + if ($event->isStopped()) { + return false; + } + + if (!$this->exists($this->getID())) { + return false; + } + + $this->_deleteDependent($id, $cascade); + $this->_deleteLinks($id); + $this->id = $id; + + if (!empty($this->belongsTo)) { + foreach ($this->belongsTo as $assoc) { + if (empty($assoc['counterCache'])) { + continue; + } + + $keys = $this->find('first', [ + 'fields' => $this->_collectForeignKeys(), + 'conditions' => [$this->alias . '.' . $this->primaryKey => $id], + 'recursive' => -1, + 'callbacks' => false + ]); + break; + } + } + + if (!$this->getDataSource()->delete($this, [$this->alias . '.' . $this->primaryKey => $id])) { + return false; + } + + if (!empty($keys[$this->alias])) { + $this->updateCounterCache($keys[$this->alias]); + } + + $this->getEventManager()->dispatch(new CakeEvent('Model.afterDelete', $this)); + $this->_clearCache(); + $this->id = false; + + return true; + } + + /** + * Cascades model deletes through associated hasMany and hasOne child records. + * + * @param string $id ID of record that was deleted + * @param bool $cascade Set to true to delete records that depend on this record + * @return void + */ + protected function _deleteDependent($id, $cascade) + { + if ($cascade !== true) { + return; + } + + if (!empty($this->__backAssociation)) { + $savedAssociations = $this->__backAssociation; + $this->__backAssociation = []; + } + + foreach (array_merge($this->hasMany, $this->hasOne) as $assoc => $data) { + if ($data['dependent'] !== true) { + continue; + } + + $Model = $this->{$assoc}; + + if ($data['foreignKey'] === false && $data['conditions'] && in_array($this->name, $Model->getAssociated('belongsTo'))) { + $Model->recursive = 0; + $conditions = [$this->escapeField(null, $this->name) => $id]; + } else { + $Model->recursive = -1; + $conditions = [$Model->escapeField($data['foreignKey']) => $id]; + if ($data['conditions']) { + $conditions = array_merge((array)$data['conditions'], $conditions); + } + } + + if (isset($data['exclusive']) && $data['exclusive']) { + $Model->deleteAll($conditions); + } else { + $records = $Model->find('all', [ + 'conditions' => $conditions, 'fields' => $Model->primaryKey + ]); + + if (!empty($records)) { + foreach ($records as $record) { + $Model->delete($record[$Model->alias][$Model->primaryKey]); + } + } + } + } + + if (isset($savedAssociations)) { + $this->__backAssociation = $savedAssociations; + } + } + + /** + * Cascades model deletes through HABTM join keys. + * + * @param string $id ID of record that was deleted + * @return void + */ + protected function _deleteLinks($id) + { + foreach ($this->hasAndBelongsToMany as $data) { + list(, $joinModel) = pluginSplit($data['with']); + $Model = $this->{$joinModel}; + $records = $Model->find('all', [ + 'conditions' => $this->_getConditionsForDeletingLinks($Model, $id, $data), + 'fields' => $Model->primaryKey, + 'recursive' => -1, + 'callbacks' => false + ]); + + if (!empty($records)) { + foreach ($records as $record) { + $Model->delete($record[$Model->alias][$Model->primaryKey]); + } + } + } + } + + /** + * Returns the conditions to be applied to Model::find() when determining which HABTM records should be deleted via + * Model::_deleteLinks() + * + * @param Model $Model HABTM join model instance + * @param mixed $id The ID of the primary model which is being deleted + * @param array $relationshipConfig The relationship config defined on the primary model + * @return array + */ + protected function _getConditionsForDeletingLinks(Model $Model, $id, array $relationshipConfig) + { + return [$Model->escapeField($relationshipConfig['foreignKey']) => $id]; + } + + /** + * Collects foreign keys from associations. + * + * @param string $type Association type. + * @return array + */ + protected function _collectForeignKeys($type = 'belongsTo') + { + $result = []; + + foreach ($this->{$type} as $assoc => $data) { + if (isset($data['foreignKey']) && is_string($data['foreignKey'])) { + $result[$assoc] = $data['foreignKey']; + } + } + + return $result; + } + + /** + * Returns true if a record that meets given conditions exists. + * + * @param array $conditions SQL conditions array + * @return bool True if such a record exists + */ + public function hasAny($conditions = null) + { + return (bool)$this->find('count', ['conditions' => $conditions, 'recursive' => -1]); + } + + /** + * Returns false if any fields passed match any (by default, all if $or = false) of their matching values. + * + * Can be used as a validation method. When used as a validation method, the `$or` parameter + * contains an array of fields to be validated. + * + * @param array $fields Field/value pairs to search (if no values specified, they are pulled from $this->data) + * @param bool|array $or If false, all fields specified must match in order for a false return value + * @return bool False if any records matching any fields are found + */ + public function isUnique($fields, $or = true) + { + if (is_array($or)) { + $isRule = ( + array_key_exists('rule', $or) && + array_key_exists('required', $or) && + array_key_exists('message', $or) + ); + if (!$isRule) { + $args = func_get_args(); + $fields = $args[1]; + $or = isset($args[2]) ? $args[2] : true; + } + } + if (!is_array($fields)) { + $fields = func_get_args(); + $fieldCount = count($fields) - 1; + if (is_bool($fields[$fieldCount])) { + $or = $fields[$fieldCount]; + unset($fields[$fieldCount]); + } + } + + foreach ($fields as $field => $value) { + if (is_numeric($field)) { + unset($fields[$field]); + + $field = $value; + $value = null; + if (isset($this->data[$this->alias][$field])) { + $value = $this->data[$this->alias][$field]; + } + } + + if (strpos($field, '.') === false) { + unset($fields[$field]); + $fields[$this->alias . '.' . $field] = $value; + } + } + + if ($or) { + $fields = ['or' => $fields]; + } + + if (!empty($this->id)) { + $fields[$this->alias . '.' . $this->primaryKey . ' !='] = $this->id; + } + + return !$this->find('count', ['conditions' => $fields, 'recursive' => -1]); + } + + /** + * Returns a resultset for a given SQL statement. Custom SQL queries should be performed with this method. + * + * The method can options 2nd and 3rd parameters. + * + * - 2nd param: Either a boolean to control query caching or an array of parameters + * for use with prepared statement placeholders. + * - 3rd param: If 2nd argument is provided, a boolean flag for enabling/disabled + * query caching. + * + * If the query cache param as 2nd or 3rd argument is not given then the model's + * default `$cacheQueries` value is used. + * + * @param string $sql SQL statement + * @return mixed Resultset array or boolean indicating success / failure depending on the query executed + * @link https://book.cakephp.org/2.0/en/models/retrieving-your-data.html#model-query + */ + public function query($sql) + { + $params = func_get_args(); + // use $this->cacheQueries as default when argument not explicitly given already + if (count($params) === 1 || count($params) === 2 && !is_bool($params[1])) { + $params[] = $this->cacheQueries; + } + $db = $this->getDataSource(); + return call_user_func_array([&$db, 'query'], $params); + } + + /** + * Returns an array of fields that have failed the validation of the current model. + * + * Additionally it populates the validationErrors property of the model with the same array. + * + * @param array|string $options An optional array of custom options to be made available in the beforeValidate callback + * @return array|bool Array of invalid fields and their error messages + * @see Model::validates() + */ + public function invalidFields($options = []) + { + return $this->validator()->errors($options); + } + + /** + * Marks a field as invalid, optionally setting the name of validation + * rule (in case of multiple validation for field) that was broken. + * + * @param string $field The name of the field to invalidate + * @param mixed $value Name of validation rule that was not failed, or validation message to + * be returned. If no validation key is provided, defaults to true. + * @return void + */ + public function invalidate($field, $value = true) + { + $this->validator()->invalidate($field, $value); + } + + /** + * Returns true if given field name is a foreign key in this model. + * + * @param string $field Returns true if the input string ends in "_id" + * @return bool True if the field is a foreign key listed in the belongsTo array. + */ + public function isForeignKey($field) + { + $foreignKeys = []; + if (!empty($this->belongsTo)) { + foreach ($this->belongsTo as $data) { + $foreignKeys[] = $data['foreignKey']; + } + } + + return in_array($field, $foreignKeys); + } + + /** + * Returns the ID of the last record this model inserted. + * + * @return mixed Last inserted ID + */ + public function getLastInsertID() + { + return $this->getInsertID(); + } + + /** + * Returns the ID of the last record this model inserted. + * + * @return mixed Last inserted ID + */ + public function getInsertID() + { + return $this->_insertID; + } + + /** + * Sets the ID of the last record this model inserted + * + * @param int|string $id Last inserted ID + * @return void + */ + public function setInsertID($id) + { + $this->_insertID = $id; + } + + /** + * Returns the number of rows returned from the last query. + * + * @return int Number of rows + */ + public function getNumRows() + { + return $this->getDataSource()->lastNumRows(); + } + + /** + * Returns the number of rows affected by the last query. + * + * @return int Number of rows + */ + public function getAffectedRows() + { + return $this->getDataSource()->lastAffected(); + } + + /** + * Get associations + * + * @return array + */ + public function associations() + { + return $this->_associations; + } + + /** + * Called before each find operation. Return false if you want to halt the find + * call, otherwise return the (modified) query data. + * + * @param array $query Data used to execute this query, i.e. conditions, order, etc. + * @return mixed true if the operation should continue, false if it should abort; or, modified + * $query to continue with new $query + * @link https://book.cakephp.org/2.0/en/models/callback-methods.html#beforefind + */ + public function beforeFind($query) + { + return true; + } + + /** + * Called after each find operation. Can be used to modify any results returned by find(). + * Return value should be the (modified) results. + * + * @param mixed $results The results of the find operation + * @param bool $primary Whether this model is being queried directly (vs. being queried as an association) + * @return mixed Result of the find operation + * @link https://book.cakephp.org/2.0/en/models/callback-methods.html#afterfind + */ + public function afterFind($results, $primary = false) + { + return $results; + } + + /** + * Called before each save operation, after validation. Return a non-true result + * to halt the save. + * + * @param array $options Options passed from Model::save(). + * @return bool True if the operation should continue, false if it should abort + * @link https://book.cakephp.org/2.0/en/models/callback-methods.html#beforesave + * @see Model::save() + */ + public function beforeSave($options = []) + { + return true; + } + + /** + * Called after each successful save operation. + * + * @param bool $created True if this save created a new record + * @param array $options Options passed from Model::save(). + * @return void + * @link https://book.cakephp.org/2.0/en/models/callback-methods.html#aftersave + * @see Model::save() + */ + public function afterSave($created, $options = []) + { + } + + /** + * Called before every deletion operation. + * + * @param bool $cascade If true records that depend on this record will also be deleted + * @return bool True if the operation should continue, false if it should abort + * @link https://book.cakephp.org/2.0/en/models/callback-methods.html#beforedelete + */ + public function beforeDelete($cascade = true) + { + return true; + } + + /** + * Called after every deletion operation. + * + * @return void + * @link https://book.cakephp.org/2.0/en/models/callback-methods.html#afterdelete + */ + public function afterDelete() + { + } + + /** + * Called during validation operations, before validation. Please note that custom + * validation rules can be defined in $validate. + * + * @param array $options Options passed from Model::save(). + * @return bool True if validate operation should continue, false to abort + * @link https://book.cakephp.org/2.0/en/models/callback-methods.html#beforevalidate + * @see Model::save() + */ + public function beforeValidate($options = []) + { + return true; + } + + /** + * Called after data has been checked for errors + * + * @return void + */ + public function afterValidate() + { + } + + /** + * Called when a DataSource-level error occurs. + * + * @return void + * @link https://book.cakephp.org/2.0/en/models/callback-methods.html#onerror + */ + public function onError() + { + } + + /** + * Handles the before/after filter logic for find('all') operations. Only called by Model::find(). + * + * @param string $state Either "before" or "after" + * @param array $query Query. + * @param array $results Results. + * @return array + * @see Model::find() + */ + protected function _findAll($state, $query, $results = []) + { + if ($state === 'before') { + return $query; + } + + return $results; + } + + /** + * Handles the before/after filter logic for find('first') operations. Only called by Model::find(). + * + * @param string $state Either "before" or "after" + * @param array $query Query. + * @param array $results Results. + * @return array + * @see Model::find() + */ + protected function _findFirst($state, $query, $results = []) + { + if ($state === 'before') { + $query['limit'] = 1; + return $query; + } + + if (empty($results[0])) { + return []; + } + + return $results[0]; + } + + /** + * Handles the before/after filter logic for find('count') operations. Only called by Model::find(). + * + * @param string $state Either "before" or "after" + * @param array $query Query. + * @param array $results Results. + * @return int|false The number of records found, or false + * @see Model::find() + */ + protected function _findCount($state, $query, $results = []) + { + if ($state === 'before') { + if (!empty($query['type']) && isset($this->findMethods[$query['type']]) && $query['type'] !== 'count') { + $query['operation'] = 'count'; + $query = $this->{'_find' . ucfirst($query['type'])}('before', $query); + } + + $db = $this->getDataSource(); + $query['order'] = false; + if (!method_exists($db, 'calculate')) { + return $query; + } + + if (!empty($query['fields']) && is_array($query['fields'])) { + if (!preg_match('/^count/i', current($query['fields']))) { + unset($query['fields']); + } + } + + if (empty($query['fields'])) { + $query['fields'] = $db->calculate($this, 'count'); + } else if (method_exists($db, 'expression') && is_string($query['fields']) && !preg_match('/count/i', $query['fields'])) { + $query['fields'] = $db->calculate($this, 'count', [ + $db->expression($query['fields']), 'count' + ]); + } + + return $query; + } + + foreach ([0, $this->alias] as $key) { + if (isset($results[0][$key]['count'])) { + if ($query['group']) { + return count($results); + } + + return (int)$results[0][$key]['count']; + } + } + + return false; + } + + /** + * Handles the before/after filter logic for find('list') operations. Only called by Model::find(). + * + * @param string $state Either "before" or "after" + * @param array $query Query. + * @param array $results Results. + * @return array Key/value pairs of primary keys/display field values of all records found + * @see Model::find() + */ + protected function _findList($state, $query, $results = []) + { + if ($state === 'before') { + if (empty($query['fields'])) { + $query['fields'] = ["{$this->alias}.{$this->primaryKey}", "{$this->alias}.{$this->displayField}"]; + $list = ["{n}.{$this->alias}.{$this->primaryKey}", "{n}.{$this->alias}.{$this->displayField}", null]; + } else { + if (!is_array($query['fields'])) { + $query['fields'] = CakeText::tokenize($query['fields']); + } + + if (count($query['fields']) === 1) { + if (strpos($query['fields'][0], '.') === false) { + $query['fields'][0] = $this->alias . '.' . $query['fields'][0]; + } + + $list = ["{n}.{$this->alias}.{$this->primaryKey}", '{n}.' . $query['fields'][0], null]; + $query['fields'] = ["{$this->alias}.{$this->primaryKey}", $query['fields'][0]]; + } else if (count($query['fields']) === 3) { + for ($i = 0; $i < 3; $i++) { + if (strpos($query['fields'][$i], '.') === false) { + $query['fields'][$i] = $this->alias . '.' . $query['fields'][$i]; + } + } + + $list = ['{n}.' . $query['fields'][0], '{n}.' . $query['fields'][1], '{n}.' . $query['fields'][2]]; + } else { + for ($i = 0; $i < 2; $i++) { + if (strpos($query['fields'][$i], '.') === false) { + $query['fields'][$i] = $this->alias . '.' . $query['fields'][$i]; + } + } + + $list = ['{n}.' . $query['fields'][0], '{n}.' . $query['fields'][1], null]; + } + } + + if (!isset($query['recursive']) || $query['recursive'] === null) { + $query['recursive'] = -1; + } + list($query['list']['keyPath'], $query['list']['valuePath'], $query['list']['groupPath']) = $list; + + return $query; + } + + if (empty($results)) { + return []; + } + + return Hash::combine($results, $query['list']['keyPath'], $query['list']['valuePath'], $query['list']['groupPath']); + } + + /** + * Detects the previous field's value, then uses logic to find the 'wrapping' + * rows and return them. + * + * @param string $state Either "before" or "after" + * @param array $query Query. + * @param array $results Results. + * @return array + */ + protected function _findNeighbors($state, $query, $results = []) + { + extract($query); + + if ($state === 'before') { + $conditions = (array)$conditions; + if (isset($field) && isset($value)) { + if (strpos($field, '.') === false) { + $field = $this->alias . '.' . $field; + } + } else { + $field = $this->alias . '.' . $this->primaryKey; + $value = $this->id; + } + + $query['conditions'] = array_merge($conditions, [$field . ' <' => $value]); + $query['order'] = $field . ' DESC'; + $query['limit'] = 1; + $query['field'] = $field; + $query['value'] = $value; + + return $query; + } + + unset($query['conditions'][$field . ' <']); + $return = []; + if (isset($results[0])) { + $prevVal = Hash::get($results[0], $field); + $query['conditions'][$field . ' >='] = $prevVal; + $query['conditions'][$field . ' !='] = $value; + $query['limit'] = 2; + } else { + $return['prev'] = null; + $query['conditions'][$field . ' >'] = $value; + $query['limit'] = 1; + } + + $query['order'] = $field . ' ASC'; + $neighbors = $this->find('all', $query); + if (!array_key_exists('prev', $return)) { + $return['prev'] = isset($neighbors[0]) ? $neighbors[0] : null; + } + + if (count($neighbors) === 2) { + $return['next'] = $neighbors[1]; + } else if (count($neighbors) === 1 && !$return['prev']) { + $return['next'] = $neighbors[0]; + } else { + $return['next'] = null; + } + + return $return; + } + + /** + * In the event of ambiguous results returned (multiple top level results, with different parent_ids) + * top level results with different parent_ids to the first result will be dropped + * + * @param string $state Either "before" or "after". + * @param array $query Query. + * @param array $results Results. + * @return array Threaded results + */ + protected function _findThreaded($state, $query, $results = []) + { + if ($state === 'before') { + return $query; + } + + $parent = 'parent_id'; + if (isset($query['parent'])) { + $parent = $query['parent']; + } + + return Hash::nest($results, [ + 'idPath' => '{n}.' . $this->alias . '.' . $this->primaryKey, + 'parentPath' => '{n}.' . $this->alias . '.' . $parent + ]); + } } diff --git a/lib/Cake/Model/ModelBehavior.php b/lib/Cake/Model/ModelBehavior.php index a5193ee0..04ffbdb1 100755 --- a/lib/Cake/Model/ModelBehavior.php +++ b/lib/Cake/Model/ModelBehavior.php @@ -32,7 +32,7 @@ * * ``` * function doSomething(Model $model, $arg1, $arg2) { - * //do something + * //do something * } * ``` * @@ -49,7 +49,7 @@ * public $mapMethods = array('/do(\w+)/' => 'doSomething'); * * function doSomething(Model $model, $method, $arg1, $arg2) { - * //do something + * //do something * } * ``` * @@ -61,180 +61,193 @@ * @see Model::$actsAs * @see BehaviorCollection::load() */ -class ModelBehavior extends CakeObject { - -/** - * Contains configuration settings for use with individual model objects. This - * is used because if multiple models use this Behavior, each will use the same - * object instance. Individual model settings should be stored as an - * associative array, keyed off of the model name. - * - * @var array - * @see Model::$alias - */ - public $settings = array(); - -/** - * Allows the mapping of preg-compatible regular expressions to public or - * private methods in this class, where the array key is a /-delimited regular - * expression, and the value is a class method. Similar to the functionality of - * the findBy* / findAllBy* magic methods. - * - * @var array - */ - public $mapMethods = array(); - -/** - * Setup this behavior with the specified configuration settings. - * - * @param Model $model Model using this behavior - * @param array $config Configuration settings for $model - * @return void - */ - public function setup(Model $model, $config = array()) { - } - -/** - * Clean up any initialization this behavior has done on a model. Called when a behavior is dynamically - * detached from a model using Model::detach(). - * - * @param Model $model Model using this behavior - * @return void - * @see BehaviorCollection::detach() - */ - public function cleanup(Model $model) { - if (isset($this->settings[$model->alias])) { - unset($this->settings[$model->alias]); - } - } - -/** - * beforeFind can be used to cancel find operations, or modify the query that will be executed. - * By returning null/false you can abort a find. By returning an array you can modify/replace the query - * that is going to be run. - * - * @param Model $model Model using this behavior - * @param array $query Data used to execute this query, i.e. conditions, order, etc. - * @return bool|array False or null will abort the operation. You can return an array to replace the - * $query that will be eventually run. - */ - public function beforeFind(Model $model, $query) { - return true; - } - -/** - * After find callback. Can be used to modify any results returned by find. - * - * @param Model $model Model using this behavior - * @param mixed $results The results of the find operation - * @param bool $primary Whether this model is being queried directly (vs. being queried as an association) - * @return mixed An array value will replace the value of $results - any other value will be ignored. - */ - public function afterFind(Model $model, $results, $primary = false) { - } - -/** - * beforeValidate is called before a model is validated, you can use this callback to - * add behavior validation rules into a models validate array. Returning false - * will allow you to make the validation fail. - * - * @param Model $model Model using this behavior - * @param array $options Options passed from Model::save(). - * @return mixed False or null will abort the operation. Any other result will continue. - * @see Model::save() - */ - public function beforeValidate(Model $model, $options = array()) { - return true; - } - -/** - * afterValidate is called just after model data was validated, you can use this callback - * to perform any data cleanup or preparation if needed - * - * @param Model $model Model using this behavior - * @return mixed False will stop this event from being passed to other behaviors - */ - public function afterValidate(Model $model) { - return true; - } - -/** - * beforeSave is called before a model is saved. Returning false from a beforeSave callback - * will abort the save operation. - * - * @param Model $model Model using this behavior - * @param array $options Options passed from Model::save(). - * @return mixed False if the operation should abort. Any other result will continue. - * @see Model::save() - */ - public function beforeSave(Model $model, $options = array()) { - return true; - } - -/** - * afterSave is called after a model is saved. - * - * @param Model $model Model using this behavior - * @param bool $created True if this save created a new record - * @param array $options Options passed from Model::save(). - * @return bool - * @see Model::save() - */ - public function afterSave(Model $model, $created, $options = array()) { - return true; - } - -/** - * Before delete is called before any delete occurs on the attached model, but after the model's - * beforeDelete is called. Returning false from a beforeDelete will abort the delete. - * - * @param Model $model Model using this behavior - * @param bool $cascade If true records that depend on this record will also be deleted - * @return mixed False if the operation should abort. Any other result will continue. - */ - public function beforeDelete(Model $model, $cascade = true) { - return true; - } - -/** - * After delete is called after any delete occurs on the attached model. - * - * @param Model $model Model using this behavior - * @return void - */ - public function afterDelete(Model $model) { - } - -/** - * DataSource error callback - * - * @param Model $model Model using this behavior - * @param string $error Error generated in DataSource - * @return void - */ - public function onError(Model $model, $error) { - } - -/** - * If $model's whitelist property is non-empty, $field will be added to it. - * Note: this method should *only* be used in beforeValidate or beforeSave to ensure - * that it only modifies the whitelist for the current save operation. Also make sure - * you explicitly set the value of the field which you are allowing. - * - * @param Model $model Model using this behavior - * @param string $field Field to be added to $model's whitelist - * @return void - */ - protected function _addToWhitelist(Model $model, $field) { - if (is_array($field)) { - foreach ($field as $f) { - $this->_addToWhitelist($model, $f); - } - return; - } - if (!empty($model->whitelist) && !in_array($field, $model->whitelist)) { - $model->whitelist[] = $field; - } - } +class ModelBehavior extends CakeObject +{ + + /** + * Contains configuration settings for use with individual model objects. This + * is used because if multiple models use this Behavior, each will use the same + * object instance. Individual model settings should be stored as an + * associative array, keyed off of the model name. + * + * @var array + * @see Model::$alias + */ + public $settings = []; + + /** + * Allows the mapping of preg-compatible regular expressions to public or + * private methods in this class, where the array key is a /-delimited regular + * expression, and the value is a class method. Similar to the functionality of + * the findBy* / findAllBy* magic methods. + * + * @var array + */ + public $mapMethods = []; + + /** + * Setup this behavior with the specified configuration settings. + * + * @param Model $model Model using this behavior + * @param array $config Configuration settings for $model + * @return void + */ + public function setup(Model $model, $config = []) + { + } + + /** + * Clean up any initialization this behavior has done on a model. Called when a behavior is dynamically + * detached from a model using Model::detach(). + * + * @param Model $model Model using this behavior + * @return void + * @see BehaviorCollection::detach() + */ + public function cleanup(Model $model) + { + if (isset($this->settings[$model->alias])) { + unset($this->settings[$model->alias]); + } + } + + /** + * beforeFind can be used to cancel find operations, or modify the query that will be executed. + * By returning null/false you can abort a find. By returning an array you can modify/replace the query + * that is going to be run. + * + * @param Model $model Model using this behavior + * @param array $query Data used to execute this query, i.e. conditions, order, etc. + * @return bool|array False or null will abort the operation. You can return an array to replace the + * $query that will be eventually run. + */ + public function beforeFind(Model $model, $query) + { + return true; + } + + /** + * After find callback. Can be used to modify any results returned by find. + * + * @param Model $model Model using this behavior + * @param mixed $results The results of the find operation + * @param bool $primary Whether this model is being queried directly (vs. being queried as an association) + * @return mixed An array value will replace the value of $results - any other value will be ignored. + */ + public function afterFind(Model $model, $results, $primary = false) + { + } + + /** + * beforeValidate is called before a model is validated, you can use this callback to + * add behavior validation rules into a models validate array. Returning false + * will allow you to make the validation fail. + * + * @param Model $model Model using this behavior + * @param array $options Options passed from Model::save(). + * @return mixed False or null will abort the operation. Any other result will continue. + * @see Model::save() + */ + public function beforeValidate(Model $model, $options = []) + { + return true; + } + + /** + * afterValidate is called just after model data was validated, you can use this callback + * to perform any data cleanup or preparation if needed + * + * @param Model $model Model using this behavior + * @return mixed False will stop this event from being passed to other behaviors + */ + public function afterValidate(Model $model) + { + return true; + } + + /** + * beforeSave is called before a model is saved. Returning false from a beforeSave callback + * will abort the save operation. + * + * @param Model $model Model using this behavior + * @param array $options Options passed from Model::save(). + * @return mixed False if the operation should abort. Any other result will continue. + * @see Model::save() + */ + public function beforeSave(Model $model, $options = []) + { + return true; + } + + /** + * afterSave is called after a model is saved. + * + * @param Model $model Model using this behavior + * @param bool $created True if this save created a new record + * @param array $options Options passed from Model::save(). + * @return bool + * @see Model::save() + */ + public function afterSave(Model $model, $created, $options = []) + { + return true; + } + + /** + * Before delete is called before any delete occurs on the attached model, but after the model's + * beforeDelete is called. Returning false from a beforeDelete will abort the delete. + * + * @param Model $model Model using this behavior + * @param bool $cascade If true records that depend on this record will also be deleted + * @return mixed False if the operation should abort. Any other result will continue. + */ + public function beforeDelete(Model $model, $cascade = true) + { + return true; + } + + /** + * After delete is called after any delete occurs on the attached model. + * + * @param Model $model Model using this behavior + * @return void + */ + public function afterDelete(Model $model) + { + } + + /** + * DataSource error callback + * + * @param Model $model Model using this behavior + * @param string $error Error generated in DataSource + * @return void + */ + public function onError(Model $model, $error) + { + } + + /** + * If $model's whitelist property is non-empty, $field will be added to it. + * Note: this method should *only* be used in beforeValidate or beforeSave to ensure + * that it only modifies the whitelist for the current save operation. Also make sure + * you explicitly set the value of the field which you are allowing. + * + * @param Model $model Model using this behavior + * @param string $field Field to be added to $model's whitelist + * @return void + */ + protected function _addToWhitelist(Model $model, $field) + { + if (is_array($field)) { + foreach ($field as $f) { + $this->_addToWhitelist($model, $f); + } + return; + } + if (!empty($model->whitelist) && !in_array($field, $model->whitelist)) { + $model->whitelist[] = $field; + } + } } diff --git a/lib/Cake/Model/ModelValidator.php b/lib/Cake/Model/ModelValidator.php index 406c0816..8f8ab1ff 100755 --- a/lib/Cake/Model/ModelValidator.php +++ b/lib/Cake/Model/ModelValidator.php @@ -31,572 +31,595 @@ * @package Cake.Model * @link https://book.cakephp.org/2.0/en/data-validation.html */ -class ModelValidator implements ArrayAccess, IteratorAggregate, Countable { - -/** - * Holds the CakeValidationSet objects array - * - * @var CakeValidationSet[] - */ - protected $_fields = array(); - -/** - * Holds the reference to the model this Validator is attached to - * - * @var Model - */ - protected $_model = array(); - -/** - * The validators $validate property, used for checking whether validation - * rules definition changed in the model and should be refreshed in this class - * - * @var array - */ - protected $_validate = array(); - -/** - * Holds the available custom callback methods, usually taken from model methods - * and behavior methods - * - * @var array - */ - protected $_methods = array(); - -/** - * Holds the available custom callback methods from the model - * - * @var array - */ - protected $_modelMethods = array(); - -/** - * Holds the list of behavior names that were attached when this object was created - * - * @var array - */ - protected $_behaviors = array(); - -/** - * Constructor - * - * @param Model $Model A reference to the Model the Validator is attached to - */ - public function __construct(Model $Model) { - $this->_model = $Model; - } - -/** - * Returns true if all fields pass validation. Will validate hasAndBelongsToMany associations - * that use the 'with' key as well. Since `Model::_saveMulti` is incapable of exiting a save operation. - * - * Will validate the currently set data. Use `Model::set()` or `Model::create()` to set the active data. - * - * @param array $options An optional array of custom options to be made available in the beforeValidate callback - * @return bool True if there are no errors - */ - public function validates($options = array()) { - $errors = $this->errors($options); - if (empty($errors) && $errors !== false) { - $errors = $this->_validateWithModels($options); - } - if (is_array($errors)) { - return count($errors) === 0; - } - return $errors; - } - -/** - * Validates a single record, as well as all its directly associated records. - * - * #### Options - * - * - atomic: If true (default), returns boolean. If false returns array. - * - fieldList: Equivalent to the $fieldList parameter in Model::save() - * - deep: If set to true, not only directly associated data , but deeper nested associated data is validated as well. - * - * Warning: This method could potentially change the passed argument `$data`, - * If you do not want this to happen, make a copy of `$data` before passing it - * to this method - * - * @param array &$data Record data to validate. This should be an array indexed by association name. - * @param array $options Options to use when validating record data (see above), See also $options of validates(). - * @return array|bool If atomic: True on success, or false on failure. - * Otherwise: array similar to the $data array passed, but values are set to true/false - * depending on whether each record validated successfully. - */ - public function validateAssociated(&$data, $options = array()) { - $model = $this->getModel(); - $options += array('atomic' => true, 'deep' => false); - $model->validationErrors = $validationErrors = $return = array(); - $model->create(null); - $return[$model->alias] = true; - if (!($model->set($data) && $model->validates($options))) { - $validationErrors[$model->alias] = $model->validationErrors; - $return[$model->alias] = false; - } - $data = $model->data; - if (!empty($options['deep']) && isset($data[$model->alias])) { - $recordData = $data[$model->alias]; - unset($data[$model->alias]); - $data += $recordData; - } - - $associations = $model->getAssociated(); - foreach ($data as $association => &$values) { - $validates = true; - if (isset($associations[$association])) { - if (in_array($associations[$association], array('belongsTo', 'hasOne'))) { - if ($options['deep']) { - $validates = $model->{$association}->validateAssociated($values, $options); - } else { - $model->{$association}->create(null); - $validates = $model->{$association}->set($values) && $model->{$association}->validates($options); - $data[$association] = $model->{$association}->data[$model->{$association}->alias]; - } - if (is_array($validates)) { - $validates = !in_array(false, Hash::flatten($validates), true); - } - $return[$association] = $validates; - } elseif ($associations[$association] === 'hasMany') { - $validates = $model->{$association}->validateMany($values, $options); - $return[$association] = $validates; - } - if (!$validates || (is_array($validates) && in_array(false, $validates, true))) { - $validationErrors[$association] = $model->{$association}->validationErrors; - } - } - } - - $model->validationErrors = $validationErrors; - if (isset($validationErrors[$model->alias])) { - $model->validationErrors = $validationErrors[$model->alias]; - unset($validationErrors[$model->alias]); - $model->validationErrors = array_merge($model->validationErrors, $validationErrors); - } - if (!$options['atomic']) { - return $return; - } - if ($return[$model->alias] === false || !empty($model->validationErrors)) { - return false; - } - return true; - } - -/** - * Validates multiple individual records for a single model - * - * #### Options - * - * - atomic: If true (default), returns boolean. If false returns array. - * - fieldList: Equivalent to the $fieldList parameter in Model::save() - * - deep: If set to true, all associated data will be validated as well. - * - * Warning: This method could potentially change the passed argument `$data`, - * If you do not want this to happen, make a copy of `$data` before passing it - * to this method - * - * @param array &$data Record data to validate. This should be a numerically-indexed array - * @param array $options Options to use when validating record data (see above), See also $options of validates(). - * @return mixed If atomic: True on success, or false on failure. - * Otherwise: array similar to the $data array passed, but values are set to true/false - * depending on whether each record validated successfully. - */ - public function validateMany(&$data, $options = array()) { - $model = $this->getModel(); - $options += array('atomic' => true, 'deep' => false); - $model->validationErrors = $validationErrors = $return = array(); - foreach ($data as $key => &$record) { - if ($options['deep']) { - $validates = $model->validateAssociated($record, $options); - } else { - $model->create(null); - $validates = $model->set($record) && $model->validates($options); - $data[$key] = $model->data; - } - if ($validates === false || (is_array($validates) && in_array(false, Hash::flatten($validates), true))) { - $validationErrors[$key] = $model->validationErrors; - $validates = false; - } else { - $validates = true; - } - $return[$key] = $validates; - } - $model->validationErrors = $validationErrors; - if (!$options['atomic']) { - return $return; - } - return empty($model->validationErrors); - } - -/** - * Returns an array of fields that have failed validation. On the current model. This method will - * actually run validation rules over data, not just return the messages. - * - * @param string $options An optional array of custom options to be made available in the beforeValidate callback - * @return array|bool Array of invalid fields - * @triggers Model.afterValidate $model - * @see ModelValidator::validates() - */ - public function errors($options = array()) { - if (!$this->_triggerBeforeValidate($options)) { - return false; - } - $model = $this->getModel(); - - if (!$this->_parseRules()) { - return $model->validationErrors; - } - - $fieldList = $model->whitelist; - if (empty($fieldList) && !empty($options['fieldList'])) { - if (!empty($options['fieldList'][$model->alias]) && is_array($options['fieldList'][$model->alias])) { - $fieldList = $options['fieldList'][$model->alias]; - } else { - $fieldList = $options['fieldList']; - } - } - - $exists = $model->exists($model->getID()); - $methods = $this->getMethods(); - $fields = $this->_validationList($fieldList); - - foreach ($fields as $field) { - $field->setMethods($methods); - $field->setValidationDomain($model->validationDomain); - $data = isset($model->data[$model->alias]) ? $model->data[$model->alias] : array(); - $errors = $field->validate($data, $exists); - foreach ($errors as $error) { - $this->invalidate($field->field, $error); - } - } - - $model->getEventManager()->dispatch(new CakeEvent('Model.afterValidate', $model)); - return $model->validationErrors; - } - -/** - * Marks a field as invalid, optionally setting a message explaining - * why the rule failed - * - * @param string $field The name of the field to invalidate - * @param string|bool $message Validation message explaining why the rule failed, defaults to true. - * @return void - */ - public function invalidate($field, $message = true) { - $this->getModel()->validationErrors[$field][] = $message; - } - -/** - * Gets all possible custom methods from the Model and attached Behaviors - * to be used as validators - * - * @return array List of callables to be used as validation methods - */ - public function getMethods() { - $behaviors = $this->_model->Behaviors->enabled(); - if (!empty($this->_methods) && $behaviors === $this->_behaviors) { - return $this->_methods; - } - $this->_behaviors = $behaviors; - - if (empty($this->_modelMethods)) { - foreach (get_class_methods($this->_model) as $method) { - $this->_modelMethods[strtolower($method)] = array($this->_model, $method); - } - } - - $methods = $this->_modelMethods; - foreach (array_keys($this->_model->Behaviors->methods()) as $method) { - $methods += array(strtolower($method) => array($this->_model, $method)); - } - - return $this->_methods = $methods; - } - -/** - * Returns a CakeValidationSet object containing all validation rules for a field, if no - * params are passed then it returns an array with all CakeValidationSet objects for each field - * - * @param string $name [optional] The fieldname to fetch. Defaults to null. - * @return CakeValidationSet|array|null - */ - public function getField($name = null) { - $this->_parseRules(); - if ($name !== null) { - if (!empty($this->_fields[$name])) { - return $this->_fields[$name]; - } - return null; - } - return $this->_fields; - } - -/** - * Sets the CakeValidationSet objects from the `Model::$validate` property - * If `Model::$validate` is not set or empty, this method returns false. True otherwise. - * - * @return bool true if `Model::$validate` was processed, false otherwise - */ - protected function _parseRules() { - if ($this->_validate === $this->_model->validate) { - return true; - } - - if (empty($this->_model->validate)) { - $this->_validate = array(); - $this->_fields = array(); - return false; - } - - $this->_validate = $this->_model->validate; - $this->_fields = array(); - $methods = $this->getMethods(); - foreach ($this->_validate as $fieldName => $ruleSet) { - $this->_fields[$fieldName] = new CakeValidationSet($fieldName, $ruleSet); - $this->_fields[$fieldName]->setMethods($methods); - } - return true; - } - -/** - * Sets the I18n domain for validation messages. This method is chainable. - * - * @param string $validationDomain [optional] The validation domain to be used. - * @return self - */ - public function setValidationDomain($validationDomain = null) { - if (empty($validationDomain)) { - $validationDomain = 'default'; - } - $this->getModel()->validationDomain = $validationDomain; - return $this; - } - -/** - * Gets the model related to this validator - * - * @return Model - */ - public function getModel() { - return $this->_model; - } - -/** - * Processes the passed fieldList and returns the list of fields to be validated - * - * @param array $fieldList list of fields to be used for validation - * @return CakeValidationSet[] List of validation rules to be applied - */ - protected function _validationList($fieldList = array()) { - if (empty($fieldList) || Hash::dimensions($fieldList) > 1) { - return $this->_fields; - } - - $validateList = array(); - $this->validationErrors = array(); - foreach ((array)$fieldList as $f) { - if (!empty($this->_fields[$f])) { - $validateList[$f] = $this->_fields[$f]; - } - } - - return $validateList; - } - -/** - * Runs validation for hasAndBelongsToMany associations that have 'with' keys - * set and data in the data set. - * - * @param array $options Array of options to use on Validation of with models - * @return bool Failure of validation on with models. - * @see Model::validates() - */ - protected function _validateWithModels($options) { - $valid = true; - $model = $this->getModel(); - - foreach ($model->hasAndBelongsToMany as $assoc => $association) { - if (empty($association['with']) || !isset($model->data[$assoc])) { - continue; - } - list($join) = $model->joinModel($model->hasAndBelongsToMany[$assoc]['with']); - $data = $model->data[$assoc]; - - $newData = array(); - foreach ((array)$data as $row) { - if (isset($row[$model->hasAndBelongsToMany[$assoc]['associationForeignKey']])) { - $newData[] = $row; - } elseif (isset($row[$join]) && isset($row[$join][$model->hasAndBelongsToMany[$assoc]['associationForeignKey']])) { - $newData[] = $row[$join]; - } - } - foreach ($newData as $data) { - $data[$model->hasAndBelongsToMany[$assoc]['foreignKey']] = $model->id; - $model->{$join}->create($data); - $valid = ($valid && $model->{$join}->validator()->validates($options)); - } - } - return $valid; - } - -/** - * Propagates beforeValidate event - * - * @param array $options Options to pass to callback. - * @return bool - * @triggers Model.beforeValidate $model, array($options) - */ - protected function _triggerBeforeValidate($options = array()) { - $model = $this->getModel(); - $event = new CakeEvent('Model.beforeValidate', $model, array($options)); - list($event->break, $event->breakOn) = array(true, false); - $model->getEventManager()->dispatch($event); - if ($event->isStopped()) { - return false; - } - return true; - } - -/** - * Returns whether a rule set is defined for a field or not - * - * @param string $field name of the field to check - * @return bool - */ - public function offsetExists($field) { - $this->_parseRules(); - return isset($this->_fields[$field]); - } - -/** - * Returns the rule set for a field - * - * @param string $field name of the field to check - * @return CakeValidationSet - */ - public function offsetGet($field) { - $this->_parseRules(); - return $this->_fields[$field]; - } - -/** - * Sets the rule set for a field - * - * @param string $field name of the field to set - * @param array|CakeValidationSet $rules set of rules to apply to field - * @return void - */ - public function offsetSet($field, $rules) { - $this->_parseRules(); - if (!$rules instanceof CakeValidationSet) { - $rules = new CakeValidationSet($field, $rules); - $methods = $this->getMethods(); - $rules->setMethods($methods); - } - $this->_fields[$field] = $rules; - } - -/** - * Unsets the rule set for a field - * - * @param string $field name of the field to unset - * @return void - */ - public function offsetUnset($field) { - $this->_parseRules(); - unset($this->_fields[$field]); - } - -/** - * Returns an iterator for each of the fields to be validated - * - * @return ArrayIterator - */ - public function getIterator() { - $this->_parseRules(); - return new ArrayIterator($this->_fields); - } - -/** - * Returns the number of fields having validation rules - * - * @return int - */ - public function count() { - $this->_parseRules(); - return count($this->_fields); - } - -/** - * Adds a new rule to a field's rule set. If second argument is an array or instance of - * CakeValidationSet then rules list for the field will be replaced with second argument and - * third argument will be ignored. - * - * ## Example: - * - * ``` - * $validator - * ->add('title', 'required', array('rule' => 'notBlank', 'required' => true)) - * ->add('user_id', 'valid', array('rule' => 'numeric', 'message' => 'Invalid User')) - * - * $validator->add('password', array( - * 'size' => array('rule' => array('lengthBetween', 8, 20)), - * 'hasSpecialCharacter' => array('rule' => 'validateSpecialchar', 'message' => 'not valid') - * )); - * ``` - * - * @param string $field The name of the field where the rule is to be added - * @param string|array|CakeValidationSet $name name of the rule to be added or list of rules for the field - * @param array|CakeValidationRule $rule or list of rules to be added to the field's rule set - * @return self - */ - public function add($field, $name, $rule = null) { - $this->_parseRules(); - if ($name instanceof CakeValidationSet) { - $this->_fields[$field] = $name; - return $this; - } - - if (!isset($this->_fields[$field])) { - $rule = (is_string($name)) ? array($name => $rule) : $name; - $this->_fields[$field] = new CakeValidationSet($field, $rule); - } else { - if (is_string($name)) { - $this->_fields[$field]->setRule($name, $rule); - } else { - $this->_fields[$field]->setRules($name); - } - } - - $methods = $this->getMethods(); - $this->_fields[$field]->setMethods($methods); - - return $this; - } - -/** - * Removes a rule from the set by its name - * - * ## Example: - * - * ``` - * $validator - * ->remove('title', 'required') - * ->remove('user_id') - * ``` - * - * @param string $field The name of the field from which the rule will be removed - * @param string $rule the name of the rule to be removed - * @return self - */ - public function remove($field, $rule = null) { - $this->_parseRules(); - if ($rule === null) { - unset($this->_fields[$field]); - } elseif (array_key_exists($field, $this->_fields)) { - $this->_fields[$field]->removeRule($rule); - } - return $this; - } +class ModelValidator implements ArrayAccess, IteratorAggregate, Countable +{ + + /** + * Holds the CakeValidationSet objects array + * + * @var CakeValidationSet[] + */ + protected $_fields = []; + + /** + * Holds the reference to the model this Validator is attached to + * + * @var Model + */ + protected $_model = []; + + /** + * The validators $validate property, used for checking whether validation + * rules definition changed in the model and should be refreshed in this class + * + * @var array + */ + protected $_validate = []; + + /** + * Holds the available custom callback methods, usually taken from model methods + * and behavior methods + * + * @var array + */ + protected $_methods = []; + + /** + * Holds the available custom callback methods from the model + * + * @var array + */ + protected $_modelMethods = []; + + /** + * Holds the list of behavior names that were attached when this object was created + * + * @var array + */ + protected $_behaviors = []; + + /** + * Constructor + * + * @param Model $Model A reference to the Model the Validator is attached to + */ + public function __construct(Model $Model) + { + $this->_model = $Model; + } + + /** + * Returns true if all fields pass validation. Will validate hasAndBelongsToMany associations + * that use the 'with' key as well. Since `Model::_saveMulti` is incapable of exiting a save operation. + * + * Will validate the currently set data. Use `Model::set()` or `Model::create()` to set the active data. + * + * @param array $options An optional array of custom options to be made available in the beforeValidate callback + * @return bool True if there are no errors + */ + public function validates($options = []) + { + $errors = $this->errors($options); + if (empty($errors) && $errors !== false) { + $errors = $this->_validateWithModels($options); + } + if (is_array($errors)) { + return count($errors) === 0; + } + return $errors; + } + + /** + * Returns an array of fields that have failed validation. On the current model. This method will + * actually run validation rules over data, not just return the messages. + * + * @param string $options An optional array of custom options to be made available in the beforeValidate callback + * @return array|bool Array of invalid fields + * @triggers Model.afterValidate $model + * @see ModelValidator::validates() + */ + public function errors($options = []) + { + if (!$this->_triggerBeforeValidate($options)) { + return false; + } + $model = $this->getModel(); + + if (!$this->_parseRules()) { + return $model->validationErrors; + } + + $fieldList = $model->whitelist; + if (empty($fieldList) && !empty($options['fieldList'])) { + if (!empty($options['fieldList'][$model->alias]) && is_array($options['fieldList'][$model->alias])) { + $fieldList = $options['fieldList'][$model->alias]; + } else { + $fieldList = $options['fieldList']; + } + } + + $exists = $model->exists($model->getID()); + $methods = $this->getMethods(); + $fields = $this->_validationList($fieldList); + + foreach ($fields as $field) { + $field->setMethods($methods); + $field->setValidationDomain($model->validationDomain); + $data = isset($model->data[$model->alias]) ? $model->data[$model->alias] : []; + $errors = $field->validate($data, $exists); + foreach ($errors as $error) { + $this->invalidate($field->field, $error); + } + } + + $model->getEventManager()->dispatch(new CakeEvent('Model.afterValidate', $model)); + return $model->validationErrors; + } + + /** + * Propagates beforeValidate event + * + * @param array $options Options to pass to callback. + * @return bool + * @triggers Model.beforeValidate $model, array($options) + */ + protected function _triggerBeforeValidate($options = []) + { + $model = $this->getModel(); + $event = new CakeEvent('Model.beforeValidate', $model, [$options]); + list($event->break, $event->breakOn) = [true, false]; + $model->getEventManager()->dispatch($event); + if ($event->isStopped()) { + return false; + } + return true; + } + + /** + * Gets the model related to this validator + * + * @return Model + */ + public function getModel() + { + return $this->_model; + } + + /** + * Sets the CakeValidationSet objects from the `Model::$validate` property + * If `Model::$validate` is not set or empty, this method returns false. True otherwise. + * + * @return bool true if `Model::$validate` was processed, false otherwise + */ + protected function _parseRules() + { + if ($this->_validate === $this->_model->validate) { + return true; + } + + if (empty($this->_model->validate)) { + $this->_validate = []; + $this->_fields = []; + return false; + } + + $this->_validate = $this->_model->validate; + $this->_fields = []; + $methods = $this->getMethods(); + foreach ($this->_validate as $fieldName => $ruleSet) { + $this->_fields[$fieldName] = new CakeValidationSet($fieldName, $ruleSet); + $this->_fields[$fieldName]->setMethods($methods); + } + return true; + } + + /** + * Gets all possible custom methods from the Model and attached Behaviors + * to be used as validators + * + * @return array List of callables to be used as validation methods + */ + public function getMethods() + { + $behaviors = $this->_model->Behaviors->enabled(); + if (!empty($this->_methods) && $behaviors === $this->_behaviors) { + return $this->_methods; + } + $this->_behaviors = $behaviors; + + if (empty($this->_modelMethods)) { + foreach (get_class_methods($this->_model) as $method) { + $this->_modelMethods[strtolower($method)] = [$this->_model, $method]; + } + } + + $methods = $this->_modelMethods; + foreach (array_keys($this->_model->Behaviors->methods()) as $method) { + $methods += [strtolower($method) => [$this->_model, $method]]; + } + + return $this->_methods = $methods; + } + + /** + * Processes the passed fieldList and returns the list of fields to be validated + * + * @param array $fieldList list of fields to be used for validation + * @return CakeValidationSet[] List of validation rules to be applied + */ + protected function _validationList($fieldList = []) + { + if (empty($fieldList) || Hash::dimensions($fieldList) > 1) { + return $this->_fields; + } + + $validateList = []; + $this->validationErrors = []; + foreach ((array)$fieldList as $f) { + if (!empty($this->_fields[$f])) { + $validateList[$f] = $this->_fields[$f]; + } + } + + return $validateList; + } + + /** + * Marks a field as invalid, optionally setting a message explaining + * why the rule failed + * + * @param string $field The name of the field to invalidate + * @param string|bool $message Validation message explaining why the rule failed, defaults to true. + * @return void + */ + public function invalidate($field, $message = true) + { + $this->getModel()->validationErrors[$field][] = $message; + } + + /** + * Runs validation for hasAndBelongsToMany associations that have 'with' keys + * set and data in the data set. + * + * @param array $options Array of options to use on Validation of with models + * @return bool Failure of validation on with models. + * @see Model::validates() + */ + protected function _validateWithModels($options) + { + $valid = true; + $model = $this->getModel(); + + foreach ($model->hasAndBelongsToMany as $assoc => $association) { + if (empty($association['with']) || !isset($model->data[$assoc])) { + continue; + } + list($join) = $model->joinModel($model->hasAndBelongsToMany[$assoc]['with']); + $data = $model->data[$assoc]; + + $newData = []; + foreach ((array)$data as $row) { + if (isset($row[$model->hasAndBelongsToMany[$assoc]['associationForeignKey']])) { + $newData[] = $row; + } else if (isset($row[$join]) && isset($row[$join][$model->hasAndBelongsToMany[$assoc]['associationForeignKey']])) { + $newData[] = $row[$join]; + } + } + foreach ($newData as $data) { + $data[$model->hasAndBelongsToMany[$assoc]['foreignKey']] = $model->id; + $model->{$join}->create($data); + $valid = ($valid && $model->{$join}->validator()->validates($options)); + } + } + return $valid; + } + + /** + * Validates a single record, as well as all its directly associated records. + * + * #### Options + * + * - atomic: If true (default), returns boolean. If false returns array. + * - fieldList: Equivalent to the $fieldList parameter in Model::save() + * - deep: If set to true, not only directly associated data , but deeper nested associated data is validated as well. + * + * Warning: This method could potentially change the passed argument `$data`, + * If you do not want this to happen, make a copy of `$data` before passing it + * to this method + * + * @param array &$data Record data to validate. This should be an array indexed by association name. + * @param array $options Options to use when validating record data (see above), See also $options of validates(). + * @return array|bool If atomic: True on success, or false on failure. + * Otherwise: array similar to the $data array passed, but values are set to true/false + * depending on whether each record validated successfully. + */ + public function validateAssociated(&$data, $options = []) + { + $model = $this->getModel(); + $options += ['atomic' => true, 'deep' => false]; + $model->validationErrors = $validationErrors = $return = []; + $model->create(null); + $return[$model->alias] = true; + if (!($model->set($data) && $model->validates($options))) { + $validationErrors[$model->alias] = $model->validationErrors; + $return[$model->alias] = false; + } + $data = $model->data; + if (!empty($options['deep']) && isset($data[$model->alias])) { + $recordData = $data[$model->alias]; + unset($data[$model->alias]); + $data += $recordData; + } + + $associations = $model->getAssociated(); + foreach ($data as $association => &$values) { + $validates = true; + if (isset($associations[$association])) { + if (in_array($associations[$association], ['belongsTo', 'hasOne'])) { + if ($options['deep']) { + $validates = $model->{$association}->validateAssociated($values, $options); + } else { + $model->{$association}->create(null); + $validates = $model->{$association}->set($values) && $model->{$association}->validates($options); + $data[$association] = $model->{$association}->data[$model->{$association}->alias]; + } + if (is_array($validates)) { + $validates = !in_array(false, Hash::flatten($validates), true); + } + $return[$association] = $validates; + } else if ($associations[$association] === 'hasMany') { + $validates = $model->{$association}->validateMany($values, $options); + $return[$association] = $validates; + } + if (!$validates || (is_array($validates) && in_array(false, $validates, true))) { + $validationErrors[$association] = $model->{$association}->validationErrors; + } + } + } + + $model->validationErrors = $validationErrors; + if (isset($validationErrors[$model->alias])) { + $model->validationErrors = $validationErrors[$model->alias]; + unset($validationErrors[$model->alias]); + $model->validationErrors = array_merge($model->validationErrors, $validationErrors); + } + if (!$options['atomic']) { + return $return; + } + if ($return[$model->alias] === false || !empty($model->validationErrors)) { + return false; + } + return true; + } + + /** + * Validates multiple individual records for a single model + * + * #### Options + * + * - atomic: If true (default), returns boolean. If false returns array. + * - fieldList: Equivalent to the $fieldList parameter in Model::save() + * - deep: If set to true, all associated data will be validated as well. + * + * Warning: This method could potentially change the passed argument `$data`, + * If you do not want this to happen, make a copy of `$data` before passing it + * to this method + * + * @param array &$data Record data to validate. This should be a numerically-indexed array + * @param array $options Options to use when validating record data (see above), See also $options of validates(). + * @return mixed If atomic: True on success, or false on failure. + * Otherwise: array similar to the $data array passed, but values are set to true/false + * depending on whether each record validated successfully. + */ + public function validateMany(&$data, $options = []) + { + $model = $this->getModel(); + $options += ['atomic' => true, 'deep' => false]; + $model->validationErrors = $validationErrors = $return = []; + foreach ($data as $key => &$record) { + if ($options['deep']) { + $validates = $model->validateAssociated($record, $options); + } else { + $model->create(null); + $validates = $model->set($record) && $model->validates($options); + $data[$key] = $model->data; + } + if ($validates === false || (is_array($validates) && in_array(false, Hash::flatten($validates), true))) { + $validationErrors[$key] = $model->validationErrors; + $validates = false; + } else { + $validates = true; + } + $return[$key] = $validates; + } + $model->validationErrors = $validationErrors; + if (!$options['atomic']) { + return $return; + } + return empty($model->validationErrors); + } + + /** + * Returns a CakeValidationSet object containing all validation rules for a field, if no + * params are passed then it returns an array with all CakeValidationSet objects for each field + * + * @param string $name [optional] The fieldname to fetch. Defaults to null. + * @return CakeValidationSet|array|null + */ + public function getField($name = null) + { + $this->_parseRules(); + if ($name !== null) { + if (!empty($this->_fields[$name])) { + return $this->_fields[$name]; + } + return null; + } + return $this->_fields; + } + + /** + * Sets the I18n domain for validation messages. This method is chainable. + * + * @param string $validationDomain [optional] The validation domain to be used. + * @return self + */ + public function setValidationDomain($validationDomain = null) + { + if (empty($validationDomain)) { + $validationDomain = 'default'; + } + $this->getModel()->validationDomain = $validationDomain; + return $this; + } + + /** + * Returns whether a rule set is defined for a field or not + * + * @param string $field name of the field to check + * @return bool + */ + public function offsetExists($field) + { + $this->_parseRules(); + return isset($this->_fields[$field]); + } + + /** + * Returns the rule set for a field + * + * @param string $field name of the field to check + * @return CakeValidationSet + */ + public function offsetGet($field) + { + $this->_parseRules(); + return $this->_fields[$field]; + } + + /** + * Sets the rule set for a field + * + * @param string $field name of the field to set + * @param array|CakeValidationSet $rules set of rules to apply to field + * @return void + */ + public function offsetSet($field, $rules) + { + $this->_parseRules(); + if (!$rules instanceof CakeValidationSet) { + $rules = new CakeValidationSet($field, $rules); + $methods = $this->getMethods(); + $rules->setMethods($methods); + } + $this->_fields[$field] = $rules; + } + + /** + * Unsets the rule set for a field + * + * @param string $field name of the field to unset + * @return void + */ + public function offsetUnset($field) + { + $this->_parseRules(); + unset($this->_fields[$field]); + } + + /** + * Returns an iterator for each of the fields to be validated + * + * @return ArrayIterator + */ + public function getIterator() + { + $this->_parseRules(); + return new ArrayIterator($this->_fields); + } + + /** + * Returns the number of fields having validation rules + * + * @return int + */ + public function count() + { + $this->_parseRules(); + return count($this->_fields); + } + + /** + * Adds a new rule to a field's rule set. If second argument is an array or instance of + * CakeValidationSet then rules list for the field will be replaced with second argument and + * third argument will be ignored. + * + * ## Example: + * + * ``` + * $validator + * ->add('title', 'required', array('rule' => 'notBlank', 'required' => true)) + * ->add('user_id', 'valid', array('rule' => 'numeric', 'message' => 'Invalid User')) + * + * $validator->add('password', array( + * 'size' => array('rule' => array('lengthBetween', 8, 20)), + * 'hasSpecialCharacter' => array('rule' => 'validateSpecialchar', 'message' => 'not valid') + * )); + * ``` + * + * @param string $field The name of the field where the rule is to be added + * @param string|array|CakeValidationSet $name name of the rule to be added or list of rules for the field + * @param array|CakeValidationRule $rule or list of rules to be added to the field's rule set + * @return self + */ + public function add($field, $name, $rule = null) + { + $this->_parseRules(); + if ($name instanceof CakeValidationSet) { + $this->_fields[$field] = $name; + return $this; + } + + if (!isset($this->_fields[$field])) { + $rule = (is_string($name)) ? [$name => $rule] : $name; + $this->_fields[$field] = new CakeValidationSet($field, $rule); + } else { + if (is_string($name)) { + $this->_fields[$field]->setRule($name, $rule); + } else { + $this->_fields[$field]->setRules($name); + } + } + + $methods = $this->getMethods(); + $this->_fields[$field]->setMethods($methods); + + return $this; + } + + /** + * Removes a rule from the set by its name + * + * ## Example: + * + * ``` + * $validator + * ->remove('title', 'required') + * ->remove('user_id') + * ``` + * + * @param string $field The name of the field from which the rule will be removed + * @param string $rule the name of the rule to be removed + * @return self + */ + public function remove($field, $rule = null) + { + $this->_parseRules(); + if ($rule === null) { + unset($this->_fields[$field]); + } else if (array_key_exists($field, $this->_fields)) { + $this->_fields[$field]->removeRule($rule); + } + return $this; + } } diff --git a/lib/Cake/Model/Permission.php b/lib/Cake/Model/Permission.php index 8014e61c..1295a590 100755 --- a/lib/Cake/Model/Permission.php +++ b/lib/Cake/Model/Permission.php @@ -21,239 +21,245 @@ * * @package Cake.Model */ -class Permission extends AppModel { +class Permission extends AppModel +{ -/** - * Explicitly disable in-memory query caching - * - * @var bool - */ - public $cacheQueries = false; + /** + * Explicitly disable in-memory query caching + * + * @var bool + */ + public $cacheQueries = false; -/** - * Override default table name - * - * @var string - */ - public $useTable = 'aros_acos'; + /** + * Override default table name + * + * @var string + */ + public $useTable = 'aros_acos'; -/** - * Permissions link AROs with ACOs - * - * @var array - */ - public $belongsTo = array('Aro', 'Aco'); + /** + * Permissions link AROs with ACOs + * + * @var array + */ + public $belongsTo = ['Aro', 'Aco']; -/** - * No behaviors for this model - * - * @var array - */ - public $actsAs = null; + /** + * No behaviors for this model + * + * @var array + */ + public $actsAs = null; -/** - * Constructor, used to tell this model to use the - * database configured for ACL - */ - public function __construct() { - $config = Configure::read('Acl.database'); - if (!empty($config)) { - $this->useDbConfig = $config; - } - parent::__construct(); - } + /** + * Constructor, used to tell this model to use the + * database configured for ACL + */ + public function __construct() + { + $config = Configure::read('Acl.database'); + if (!empty($config)) { + $this->useDbConfig = $config; + } + parent::__construct(); + } -/** - * Checks if the given $aro has access to action $action in $aco - * - * @param string $aro ARO The requesting object identifier. - * @param string $aco ACO The controlled object identifier. - * @param string $action Action (defaults to *) - * @return bool Success (true if ARO has access to action in ACO, false otherwise) - */ - public function check($aro, $aco, $action = '*') { - if (!$aro || !$aco) { - return false; - } + /** + * Checks if the given $aro has access to action $action in $aco + * + * @param string $aro ARO The requesting object identifier. + * @param string $aco ACO The controlled object identifier. + * @param string $action Action (defaults to *) + * @return bool Success (true if ARO has access to action in ACO, false otherwise) + */ + public function check($aro, $aco, $action = '*') + { + if (!$aro || !$aco) { + return false; + } - $permKeys = $this->getAcoKeys($this->schema()); - $aroPath = $this->Aro->node($aro); - $acoPath = $this->Aco->node($aco); + $permKeys = $this->getAcoKeys($this->schema()); + $aroPath = $this->Aro->node($aro); + $acoPath = $this->Aco->node($aco); - if (!$aroPath) { - $this->log(__d('cake_dev', - "%s - Failed ARO node lookup in permissions check. Node references:\nAro: %s\nAco: %s", - 'DbAcl::check()', - print_r($aro, true), - print_r($aco, true)), - E_USER_WARNING - ); - return false; - } + if (!$aroPath) { + $this->log(__d('cake_dev', + "%s - Failed ARO node lookup in permissions check. Node references:\nAro: %s\nAco: %s", + 'DbAcl::check()', + print_r($aro, true), + print_r($aco, true)), + E_USER_WARNING + ); + return false; + } - if (!$acoPath) { - $this->log(__d('cake_dev', - "%s - Failed ACO node lookup in permissions check. Node references:\nAro: %s\nAco: %s", - 'DbAcl::check()', - print_r($aro, true), - print_r($aco, true)), - E_USER_WARNING - ); - return false; - } + if (!$acoPath) { + $this->log(__d('cake_dev', + "%s - Failed ACO node lookup in permissions check. Node references:\nAro: %s\nAco: %s", + 'DbAcl::check()', + print_r($aro, true), + print_r($aco, true)), + E_USER_WARNING + ); + return false; + } - if ($action !== '*' && !in_array('_' . $action, $permKeys)) { - $this->log(__d('cake_dev', "ACO permissions key %s does not exist in %s", $action, 'DbAcl::check()'), E_USER_NOTICE); - return false; - } + if ($action !== '*' && !in_array('_' . $action, $permKeys)) { + $this->log(__d('cake_dev', "ACO permissions key %s does not exist in %s", $action, 'DbAcl::check()'), E_USER_NOTICE); + return false; + } - $acoIDs = Hash::extract($acoPath, '{n}.' . $this->Aco->alias . '.id'); + $acoIDs = Hash::extract($acoPath, '{n}.' . $this->Aco->alias . '.id'); - $count = count($aroPath); - $inherited = array(); - for ($i = 0; $i < $count; $i++) { - $permAlias = $this->alias; + $count = count($aroPath); + $inherited = []; + for ($i = 0; $i < $count; $i++) { + $permAlias = $this->alias; - $perms = $this->find('all', array( - 'conditions' => array( - "{$permAlias}.aro_id" => $aroPath[$i][$this->Aro->alias]['id'], - "{$permAlias}.aco_id" => $acoIDs - ), - 'order' => array($this->Aco->alias . '.lft' => 'desc'), - 'recursive' => 0 - )); + $perms = $this->find('all', [ + 'conditions' => [ + "{$permAlias}.aro_id" => $aroPath[$i][$this->Aro->alias]['id'], + "{$permAlias}.aco_id" => $acoIDs + ], + 'order' => [$this->Aco->alias . '.lft' => 'desc'], + 'recursive' => 0 + ]); - if (empty($perms)) { - continue; - } - $perms = Hash::extract($perms, '{n}.' . $this->alias); - foreach ($perms as $perm) { - if ($action === '*') { - if (empty($perm)) { - continue; - } - foreach ($permKeys as $key) { - if ($perm[$key] == -1 && !(isset($inherited[$key]) && $inherited[$key] == 1)) { - // Deny, but only if a child node didnt't explicitly allow - return false; - } elseif ($perm[$key] == 1) { - // Allow & inherit from parent nodes - $inherited[$key] = $perm[$key]; - } - } - } else { - switch ($perm['_' . $action]) { - case -1: - return false; - case 0: - break; - case 1: - return true; - } - } - } + if (empty($perms)) { + continue; + } + $perms = Hash::extract($perms, '{n}.' . $this->alias); + foreach ($perms as $perm) { + if ($action === '*') { + if (empty($perm)) { + continue; + } + foreach ($permKeys as $key) { + if ($perm[$key] == -1 && !(isset($inherited[$key]) && $inherited[$key] == 1)) { + // Deny, but only if a child node didnt't explicitly allow + return false; + } else if ($perm[$key] == 1) { + // Allow & inherit from parent nodes + $inherited[$key] = $perm[$key]; + } + } + } else { + switch ($perm['_' . $action]) { + case -1: + return false; + case 0: + break; + case 1: + return true; + } + } + } - if ($action === '*' && count($inherited) === count($permKeys)) { - return true; - } - } - return false; - } + if ($action === '*' && count($inherited) === count($permKeys)) { + return true; + } + } + return false; + } -/** - * Allow $aro to have access to action $actions in $aco - * - * @param string $aro ARO The requesting object identifier. - * @param string $aco ACO The controlled object identifier. - * @param string $actions Action (defaults to *) Invalid permissions will result in an exception - * @param int $value Value to indicate access type (1 to give access, -1 to deny, 0 to inherit) - * @return bool Success - * @throws AclException on Invalid permission key. - */ - public function allow($aro, $aco, $actions = '*', $value = 1) { - $perms = $this->getAclLink($aro, $aco); - $permKeys = $this->getAcoKeys($this->schema()); - $save = array(); + /** + * Get the crud type keys + * + * @param array $keys Permission schema + * @return array permission keys + */ + public function getAcoKeys($keys) + { + $newKeys = []; + $keys = array_keys($keys); + foreach ($keys as $key) { + if (!in_array($key, ['id', 'aro_id', 'aco_id'])) { + $newKeys[] = $key; + } + } + return $newKeys; + } - if (!$perms) { - $this->log(__d('cake_dev', '%s - Invalid node', 'DbAcl::allow()'), E_USER_WARNING); - return false; - } - if (isset($perms[0])) { - $save = $perms[0][$this->alias]; - } + /** + * Allow $aro to have access to action $actions in $aco + * + * @param string $aro ARO The requesting object identifier. + * @param string $aco ACO The controlled object identifier. + * @param string $actions Action (defaults to *) Invalid permissions will result in an exception + * @param int $value Value to indicate access type (1 to give access, -1 to deny, 0 to inherit) + * @return bool Success + * @throws AclException on Invalid permission key. + */ + public function allow($aro, $aco, $actions = '*', $value = 1) + { + $perms = $this->getAclLink($aro, $aco); + $permKeys = $this->getAcoKeys($this->schema()); + $save = []; - if ($actions === '*') { - $save = array_combine($permKeys, array_pad(array(), count($permKeys), $value)); - } else { - if (!is_array($actions)) { - $actions = array('_' . $actions); - } - foreach ($actions as $action) { - if ($action{0} !== '_') { - $action = '_' . $action; - } - if (!in_array($action, $permKeys, true)) { - throw new AclException(__d('cake_dev', 'Invalid permission key "%s"', $action)); - } - $save[$action] = $value; - } - } - list($save['aro_id'], $save['aco_id']) = array($perms['aro'], $perms['aco']); + if (!$perms) { + $this->log(__d('cake_dev', '%s - Invalid node', 'DbAcl::allow()'), E_USER_WARNING); + return false; + } + if (isset($perms[0])) { + $save = $perms[0][$this->alias]; + } - if ($perms['link'] && !empty($perms['link'])) { - $save['id'] = $perms['link'][0][$this->alias]['id']; - } else { - unset($save['id']); - $this->id = null; - } - return ($this->save($save) !== false); - } + if ($actions === '*') { + $save = array_combine($permKeys, array_pad([], count($permKeys), $value)); + } else { + if (!is_array($actions)) { + $actions = ['_' . $actions]; + } + foreach ($actions as $action) { + if ($action{0} !== '_') { + $action = '_' . $action; + } + if (!in_array($action, $permKeys, true)) { + throw new AclException(__d('cake_dev', 'Invalid permission key "%s"', $action)); + } + $save[$action] = $value; + } + } + list($save['aro_id'], $save['aco_id']) = [$perms['aro'], $perms['aco']]; -/** - * Get an array of access-control links between the given Aro and Aco - * - * @param string $aro ARO The requesting object identifier. - * @param string $aco ACO The controlled object identifier. - * @return array Indexed array with: 'aro', 'aco' and 'link' - */ - public function getAclLink($aro, $aco) { - $obj = array(); - $obj['Aro'] = $this->Aro->node($aro); - $obj['Aco'] = $this->Aco->node($aco); + if ($perms['link'] && !empty($perms['link'])) { + $save['id'] = $perms['link'][0][$this->alias]['id']; + } else { + unset($save['id']); + $this->id = null; + } + return ($this->save($save) !== false); + } - if (empty($obj['Aro']) || empty($obj['Aco'])) { - return false; - } - $aro = Hash::extract($obj, 'Aro.0.' . $this->Aro->alias . '.id'); - $aco = Hash::extract($obj, 'Aco.0.' . $this->Aco->alias . '.id'); - $aro = current($aro); - $aco = current($aco); + /** + * Get an array of access-control links between the given Aro and Aco + * + * @param string $aro ARO The requesting object identifier. + * @param string $aco ACO The controlled object identifier. + * @return array Indexed array with: 'aro', 'aco' and 'link' + */ + public function getAclLink($aro, $aco) + { + $obj = []; + $obj['Aro'] = $this->Aro->node($aro); + $obj['Aco'] = $this->Aco->node($aco); - return array( - 'aro' => $aro, - 'aco' => $aco, - 'link' => $this->find('all', array('conditions' => array( - $this->alias . '.aro_id' => $aro, - $this->alias . '.aco_id' => $aco - ))) - ); - } + if (empty($obj['Aro']) || empty($obj['Aco'])) { + return false; + } + $aro = Hash::extract($obj, 'Aro.0.' . $this->Aro->alias . '.id'); + $aco = Hash::extract($obj, 'Aco.0.' . $this->Aco->alias . '.id'); + $aro = current($aro); + $aco = current($aco); -/** - * Get the crud type keys - * - * @param array $keys Permission schema - * @return array permission keys - */ - public function getAcoKeys($keys) { - $newKeys = array(); - $keys = array_keys($keys); - foreach ($keys as $key) { - if (!in_array($key, array('id', 'aro_id', 'aco_id'))) { - $newKeys[] = $key; - } - } - return $newKeys; - } + return [ + 'aro' => $aro, + 'aco' => $aco, + 'link' => $this->find('all', ['conditions' => [ + $this->alias . '.aro_id' => $aro, + $this->alias . '.aco_id' => $aco + ]]) + ]; + } } diff --git a/lib/Cake/Model/Validator/CakeValidationRule.php b/lib/Cake/Model/Validator/CakeValidationRule.php index d27c7b46..933a7795 100755 --- a/lib/Cake/Model/Validator/CakeValidationRule.php +++ b/lib/Cake/Model/Validator/CakeValidationRule.php @@ -27,324 +27,331 @@ * @package Cake.Model.Validator * @link https://book.cakephp.org/2.0/en/data-validation.html */ -class CakeValidationRule { - -/** - * Whether the field passed this validation rule - * - * @var mixed - */ - protected $_valid = true; - -/** - * Holds whether the record being validated exists in datasource or not - * - * @var bool - */ - protected $_recordExists = false; - -/** - * Validation method - * - * @var mixed - */ - protected $_rule = null; - -/** - * Validation method arguments - * - * @var array - */ - protected $_ruleParams = array(); - -/** - * Holds passed in options - * - * @var array - */ - protected $_passedOptions = array(); - -/** - * The 'rule' key - * - * @var mixed - */ - public $rule = 'blank'; - -/** - * The 'required' key - * - * @var mixed - */ - public $required = null; - -/** - * The 'allowEmpty' key - * - * @var bool - */ - public $allowEmpty = null; - -/** - * The 'on' key - * - * @var string - */ - public $on = null; - -/** - * The 'last' key - * - * @var bool - */ - public $last = true; - -/** - * The 'message' key - * - * @var string - */ - public $message = null; - -/** - * Constructor - * - * @param array $validator [optional] The validator properties - */ - public function __construct($validator = array()) { - $this->_addValidatorProps($validator); - } - -/** - * Checks if the rule is valid - * - * @return bool - */ - public function isValid() { - if (!$this->_valid || (is_string($this->_valid) && !empty($this->_valid))) { - return false; - } - - return true; - } - -/** - * Returns whether the field can be left blank according to this rule - * - * @return bool - */ - public function isEmptyAllowed() { - return $this->skip() || $this->allowEmpty === true; - } - -/** - * Checks if the field is required according to the `required` property - * - * @return bool - */ - public function isRequired() { - if (in_array($this->required, array('create', 'update'), true)) { - if ($this->required === 'create' && !$this->isUpdate() || $this->required === 'update' && $this->isUpdate()) { - return true; - } - return false; - } - - return $this->required; - } - -/** - * Checks whether the field failed the `field should be present` validation - * - * @param string $field Field name - * @param array &$data Data to check rule against - * @return bool - */ - public function checkRequired($field, &$data) { - return ( - (!array_key_exists($field, $data) && $this->isRequired() === true) || - ( - array_key_exists($field, $data) && (empty($data[$field]) && - !is_numeric($data[$field])) && $this->allowEmpty === false - ) - ); - } - -/** - * Checks if the allowEmpty key applies - * - * @param string $field Field name - * @param array &$data data to check rule against - * @return bool - */ - public function checkEmpty($field, &$data) { - if (empty($data[$field]) && $data[$field] != '0' && $this->allowEmpty === true) { - return true; - } - return false; - } - -/** - * Checks if the validation rule should be skipped - * - * @return bool True if the ValidationRule can be skipped - */ - public function skip() { - if (!empty($this->on)) { - if ($this->on === 'create' && $this->isUpdate() || $this->on === 'update' && !$this->isUpdate()) { - return true; - } - } - return false; - } - -/** - * Returns whether this rule should break validation process for associated field - * after it fails - * - * @return bool - */ - public function isLast() { - return (bool)$this->last; - } - -/** - * Gets the validation error message - * - * @return string - */ - public function getValidationResult() { - return $this->_valid; - } - -/** - * Gets an array with the rule properties - * - * @return array - */ - protected function _getPropertiesArray() { - $rule = $this->rule; - if (!is_string($rule)) { - unset($rule[0]); - } - return array( - 'rule' => $rule, - 'required' => $this->required, - 'allowEmpty' => $this->allowEmpty, - 'on' => $this->on, - 'last' => $this->last, - 'message' => $this->message - ); - } - -/** - * Sets the recordExists configuration value for this rule, - * ir refers to whether the model record it is validating exists - * exists in the collection or not (create or update operation) - * - * If called with no parameters it will return whether this rule - * is configured for update operations or not. - * - * @param bool $exists Boolean to indicate if records exists - * @return bool - */ - public function isUpdate($exists = null) { - if ($exists === null) { - return $this->_recordExists; - } - return $this->_recordExists = $exists; - } - -/** - * Dispatches the validation rule to the given validator method - * - * @param string $field Field name - * @param array &$data Data array - * @param array &$methods Methods list - * @return bool True if the rule could be dispatched, false otherwise - */ - public function process($field, &$data, &$methods) { - $this->_valid = true; - $this->_parseRule($field, $data); - - $validator = $this->_getPropertiesArray(); - $rule = strtolower($this->_rule); - if (isset($methods[$rule])) { - $this->_ruleParams[] = array_merge($validator, $this->_passedOptions); - $this->_ruleParams[0] = array($field => $this->_ruleParams[0]); - $this->_valid = call_user_func_array($methods[$rule], $this->_ruleParams); - } elseif (class_exists('Validation') && method_exists('Validation', $this->_rule)) { - $this->_valid = call_user_func_array(array('Validation', $this->_rule), $this->_ruleParams); - } elseif (is_string($validator['rule'])) { - $this->_valid = preg_match($this->_rule, $data[$field]); - } else { - trigger_error(__d('cake_dev', 'Could not find validation handler %s for %s', $this->_rule, $field), E_USER_WARNING); - return false; - } - - return true; - } - -/** - * Resets internal state for this rule, by default it will become valid - * and it will set isUpdate() to false - * - * @return void - */ - public function reset() { - $this->_valid = true; - $this->_recordExists = false; - } - -/** - * Returns passed options for this rule - * - * @param string|int $key Array index - * @return array|null - */ - public function getOptions($key) { - if (!isset($this->_passedOptions[$key])) { - return null; - } - return $this->_passedOptions[$key]; - } - -/** - * Sets the rule properties from the rule entry in validate - * - * @param array $validator [optional] - * @return void - */ - protected function _addValidatorProps($validator = array()) { - if (!is_array($validator)) { - $validator = array('rule' => $validator); - } - foreach ($validator as $key => $value) { - if (isset($value) || !empty($value)) { - if (in_array($key, array('rule', 'required', 'allowEmpty', 'on', 'message', 'last'))) { - $this->{$key} = $validator[$key]; - } else { - $this->_passedOptions[$key] = $value; - } - } - } - } - -/** - * Parses the rule and sets the rule and ruleParams - * - * @param string $field Field name - * @param array &$data Data array - * @return void - */ - protected function _parseRule($field, &$data) { - if (is_array($this->rule)) { - $this->_rule = $this->rule[0]; - $this->_ruleParams = array_merge(array($data[$field]), array_values(array_slice($this->rule, 1))); - } else { - $this->_rule = $this->rule; - $this->_ruleParams = array($data[$field]); - } - } +class CakeValidationRule +{ + + /** + * The 'rule' key + * + * @var mixed + */ + public $rule = 'blank'; + /** + * The 'required' key + * + * @var mixed + */ + public $required = null; + /** + * The 'allowEmpty' key + * + * @var bool + */ + public $allowEmpty = null; + /** + * The 'on' key + * + * @var string + */ + public $on = null; + /** + * The 'last' key + * + * @var bool + */ + public $last = true; + /** + * The 'message' key + * + * @var string + */ + public $message = null; + /** + * Whether the field passed this validation rule + * + * @var mixed + */ + protected $_valid = true; + /** + * Holds whether the record being validated exists in datasource or not + * + * @var bool + */ + protected $_recordExists = false; + /** + * Validation method + * + * @var mixed + */ + protected $_rule = null; + /** + * Validation method arguments + * + * @var array + */ + protected $_ruleParams = []; + /** + * Holds passed in options + * + * @var array + */ + protected $_passedOptions = []; + + /** + * Constructor + * + * @param array $validator [optional] The validator properties + */ + public function __construct($validator = []) + { + $this->_addValidatorProps($validator); + } + + /** + * Sets the rule properties from the rule entry in validate + * + * @param array $validator [optional] + * @return void + */ + protected function _addValidatorProps($validator = []) + { + if (!is_array($validator)) { + $validator = ['rule' => $validator]; + } + foreach ($validator as $key => $value) { + if (isset($value) || !empty($value)) { + if (in_array($key, ['rule', 'required', 'allowEmpty', 'on', 'message', 'last'])) { + $this->{$key} = $validator[$key]; + } else { + $this->_passedOptions[$key] = $value; + } + } + } + } + + /** + * Checks if the rule is valid + * + * @return bool + */ + public function isValid() + { + if (!$this->_valid || (is_string($this->_valid) && !empty($this->_valid))) { + return false; + } + + return true; + } + + /** + * Returns whether the field can be left blank according to this rule + * + * @return bool + */ + public function isEmptyAllowed() + { + return $this->skip() || $this->allowEmpty === true; + } + + /** + * Checks if the validation rule should be skipped + * + * @return bool True if the ValidationRule can be skipped + */ + public function skip() + { + if (!empty($this->on)) { + if ($this->on === 'create' && $this->isUpdate() || $this->on === 'update' && !$this->isUpdate()) { + return true; + } + } + return false; + } + + /** + * Sets the recordExists configuration value for this rule, + * ir refers to whether the model record it is validating exists + * exists in the collection or not (create or update operation) + * + * If called with no parameters it will return whether this rule + * is configured for update operations or not. + * + * @param bool $exists Boolean to indicate if records exists + * @return bool + */ + public function isUpdate($exists = null) + { + if ($exists === null) { + return $this->_recordExists; + } + return $this->_recordExists = $exists; + } + + /** + * Checks whether the field failed the `field should be present` validation + * + * @param string $field Field name + * @param array &$data Data to check rule against + * @return bool + */ + public function checkRequired($field, &$data) + { + return ( + (!array_key_exists($field, $data) && $this->isRequired() === true) || + ( + array_key_exists($field, $data) && (empty($data[$field]) && + !is_numeric($data[$field])) && $this->allowEmpty === false + ) + ); + } + + /** + * Checks if the field is required according to the `required` property + * + * @return bool + */ + public function isRequired() + { + if (in_array($this->required, ['create', 'update'], true)) { + if ($this->required === 'create' && !$this->isUpdate() || $this->required === 'update' && $this->isUpdate()) { + return true; + } + return false; + } + + return $this->required; + } + + /** + * Checks if the allowEmpty key applies + * + * @param string $field Field name + * @param array &$data data to check rule against + * @return bool + */ + public function checkEmpty($field, &$data) + { + if (empty($data[$field]) && $data[$field] != '0' && $this->allowEmpty === true) { + return true; + } + return false; + } + + /** + * Returns whether this rule should break validation process for associated field + * after it fails + * + * @return bool + */ + public function isLast() + { + return (bool)$this->last; + } + + /** + * Gets the validation error message + * + * @return string + */ + public function getValidationResult() + { + return $this->_valid; + } + + /** + * Dispatches the validation rule to the given validator method + * + * @param string $field Field name + * @param array &$data Data array + * @param array &$methods Methods list + * @return bool True if the rule could be dispatched, false otherwise + */ + public function process($field, &$data, &$methods) + { + $this->_valid = true; + $this->_parseRule($field, $data); + + $validator = $this->_getPropertiesArray(); + $rule = strtolower($this->_rule); + if (isset($methods[$rule])) { + $this->_ruleParams[] = array_merge($validator, $this->_passedOptions); + $this->_ruleParams[0] = [$field => $this->_ruleParams[0]]; + $this->_valid = call_user_func_array($methods[$rule], $this->_ruleParams); + } else if (class_exists('Validation') && method_exists('Validation', $this->_rule)) { + $this->_valid = call_user_func_array(['Validation', $this->_rule], $this->_ruleParams); + } else if (is_string($validator['rule'])) { + $this->_valid = preg_match($this->_rule, $data[$field]); + } else { + trigger_error(__d('cake_dev', 'Could not find validation handler %s for %s', $this->_rule, $field), E_USER_WARNING); + return false; + } + + return true; + } + + /** + * Parses the rule and sets the rule and ruleParams + * + * @param string $field Field name + * @param array &$data Data array + * @return void + */ + protected function _parseRule($field, &$data) + { + if (is_array($this->rule)) { + $this->_rule = $this->rule[0]; + $this->_ruleParams = array_merge([$data[$field]], array_values(array_slice($this->rule, 1))); + } else { + $this->_rule = $this->rule; + $this->_ruleParams = [$data[$field]]; + } + } + + /** + * Gets an array with the rule properties + * + * @return array + */ + protected function _getPropertiesArray() + { + $rule = $this->rule; + if (!is_string($rule)) { + unset($rule[0]); + } + return [ + 'rule' => $rule, + 'required' => $this->required, + 'allowEmpty' => $this->allowEmpty, + 'on' => $this->on, + 'last' => $this->last, + 'message' => $this->message + ]; + } + + /** + * Resets internal state for this rule, by default it will become valid + * and it will set isUpdate() to false + * + * @return void + */ + public function reset() + { + $this->_valid = true; + $this->_recordExists = false; + } + + /** + * Returns passed options for this rule + * + * @param string|int $key Array index + * @return array|null + */ + public function getOptions($key) + { + if (!isset($this->_passedOptions[$key])) { + return null; + } + return $this->_passedOptions[$key]; + } } diff --git a/lib/Cake/Model/Validator/CakeValidationSet.php b/lib/Cake/Model/Validator/CakeValidationSet.php index eab54086..9682c4b4 100755 --- a/lib/Cake/Model/Validator/CakeValidationSet.php +++ b/lib/Cake/Model/Validator/CakeValidationSet.php @@ -27,344 +27,358 @@ * @package Cake.Model.Validator * @link https://book.cakephp.org/2.0/en/data-validation.html */ -class CakeValidationSet implements ArrayAccess, IteratorAggregate, Countable { - -/** - * Holds the CakeValidationRule objects - * - * @var CakeValidationRule[] - */ - protected $_rules = array(); - -/** - * List of methods available for validation - * - * @var array - */ - protected $_methods = array(); - -/** - * I18n domain for validation messages. - * - * @var string - */ - protected $_validationDomain = null; - -/** - * Whether the validation is stopped - * - * @var bool - */ - public $isStopped = false; - -/** - * Holds the fieldname - * - * @var string - */ - public $field = null; - -/** - * Holds the original ruleSet - * - * @var array - */ - public $ruleSet = array(); - -/** - * Constructor - * - * @param string $fieldName The fieldname. - * @param array $ruleSet Rules set. - */ - public function __construct($fieldName, $ruleSet) { - $this->field = $fieldName; - - if (!is_array($ruleSet) || (is_array($ruleSet) && isset($ruleSet['rule']))) { - $ruleSet = array($ruleSet); - } - - foreach ($ruleSet as $index => $validateProp) { - $this->_rules[$index] = new CakeValidationRule($validateProp); - } - $this->ruleSet = $ruleSet; - } - -/** - * Sets the list of methods to use for validation - * - * @param array &$methods Methods list - * @return void - */ - public function setMethods(&$methods) { - $this->_methods =& $methods; - } - -/** - * Sets the I18n domain for validation messages. - * - * @param string $validationDomain The validation domain to be used. - * @return void - */ - public function setValidationDomain($validationDomain) { - $this->_validationDomain = $validationDomain; - } - -/** - * Runs all validation rules in this set and returns a list of - * validation errors - * - * @param array $data Data array - * @param bool $isUpdate Is record being updated or created - * @return array list of validation errors for this field - */ - public function validate($data, $isUpdate = false) { - $this->reset(); - $errors = array(); - foreach ($this->getRules() as $name => $rule) { - $rule->isUpdate($isUpdate); - if ($rule->skip()) { - continue; - } - - $checkRequired = $rule->checkRequired($this->field, $data); - if (!$checkRequired && array_key_exists($this->field, $data)) { - if ($rule->checkEmpty($this->field, $data)) { - break; - } - $rule->process($this->field, $data, $this->_methods); - } - - if ($checkRequired || !$rule->isValid()) { - $errors[] = $this->_processValidationResponse($name, $rule); - if ($rule->isLast()) { - break; - } - } - } - - return $errors; - } - -/** - * Resets internal state for all validation rules in this set - * - * @return void - */ - public function reset() { - foreach ($this->getRules() as $rule) { - $rule->reset(); - } - } - -/** - * Gets a rule for a given name if exists - * - * @param string $name Field name. - * @return CakeValidationRule - */ - public function getRule($name) { - if (!empty($this->_rules[$name])) { - return $this->_rules[$name]; - } - } - -/** - * Returns all rules for this validation set - * - * @return CakeValidationRule[] - */ - public function getRules() { - return $this->_rules; - } - -/** - * Sets a CakeValidationRule $rule with a $name - * - * ## Example: - * - * ``` - * $set - * ->setRule('required', array('rule' => 'notBlank', 'required' => true)) - * ->setRule('between', array('rule' => array('lengthBetween', 4, 10)) - * ``` - * - * @param string $name The name under which the rule should be set - * @param CakeValidationRule|array $rule The validation rule to be set - * @return self - */ - public function setRule($name, $rule) { - if (!($rule instanceof CakeValidationRule)) { - $rule = new CakeValidationRule($rule); - } - $this->_rules[$name] = $rule; - return $this; - } - -/** - * Removes a validation rule from the set - * - * ## Example: - * - * ``` - * $set - * ->removeRule('required') - * ->removeRule('inRange') - * ``` - * - * @param string $name The name under which the rule should be unset - * @return self - */ - public function removeRule($name) { - unset($this->_rules[$name]); - return $this; - } - -/** - * Sets the rules for a given field - * - * ## Example: - * - * ``` - * $set->setRules(array( - * 'required' => array('rule' => 'notBlank', 'required' => true), - * 'inRange' => array('rule' => array('between', 4, 10) - * )); - * ``` - * - * @param array $rules The rules to be set - * @param bool $mergeVars [optional] If true, merges vars instead of replace. Defaults to true. - * @return self - */ - public function setRules($rules = array(), $mergeVars = true) { - if ($mergeVars === false) { - $this->_rules = array(); - } - foreach ($rules as $name => $rule) { - $this->setRule($name, $rule); - } - return $this; - } - -/** - * Fetches the correct error message for a failed validation - * - * @param string $name the name of the rule as it was configured - * @param CakeValidationRule $rule the object containing validation information - * @return string - */ - protected function _processValidationResponse($name, $rule) { - $message = $rule->getValidationResult(); - if (is_string($message)) { - return $message; - } - $message = $rule->message; - - if ($message !== null) { - $args = null; - if (is_array($message)) { - $result = $message[0]; - $args = array_slice($message, 1); - } else { - $result = $message; - } - if (is_array($rule->rule) && $args === null) { - $args = array_slice($rule->rule, 1); - } - $args = $this->_translateArgs($args); - - $message = __d($this->_validationDomain, $result, $args); - } elseif (is_string($name)) { - if (is_array($rule->rule)) { - $args = array_slice($rule->rule, 1); - $args = $this->_translateArgs($args); - $message = __d($this->_validationDomain, $name, $args); - } else { - $message = __d($this->_validationDomain, $name); - } - } else { - $message = __d('cake', 'This field cannot be left blank'); - } - - return $message; - } - -/** - * Applies translations to validator arguments. - * - * @param array $args The args to translate - * @return array Translated args. - */ - protected function _translateArgs($args) { - foreach ((array)$args as $k => $arg) { - if (is_string($arg)) { - $args[$k] = __d($this->_validationDomain, $arg); - } - } - return $args; - } - -/** - * Returns whether an index exists in the rule set - * - * @param string $index name of the rule - * @return bool - */ - public function offsetExists($index) { - return isset($this->_rules[$index]); - } - -/** - * Returns a rule object by its index - * - * @param string $index name of the rule - * @return CakeValidationRule - */ - public function offsetGet($index) { - return $this->_rules[$index]; - } - -/** - * Sets or replace a validation rule. - * - * This is a wrapper for ArrayAccess. Use setRule() directly for - * chainable access. - * - * @param string $index Name of the rule. - * @param CakeValidationRule|array $rule Rule to add to $index. - * @return void - * @see http://www.php.net/manual/en/arrayobject.offsetset.php - */ - public function offsetSet($index, $rule) { - $this->setRule($index, $rule); - } - -/** - * Unsets a validation rule - * - * @param string $index name of the rule - * @return void - */ - public function offsetUnset($index) { - unset($this->_rules[$index]); - } - -/** - * Returns an iterator for each of the rules to be applied - * - * @return ArrayIterator - */ - public function getIterator() { - return new ArrayIterator($this->_rules); - } - -/** - * Returns the number of rules in this set - * - * @return int - */ - public function count() { - return count($this->_rules); - } +class CakeValidationSet implements ArrayAccess, IteratorAggregate, Countable +{ + + /** + * Whether the validation is stopped + * + * @var bool + */ + public $isStopped = false; + /** + * Holds the fieldname + * + * @var string + */ + public $field = null; + /** + * Holds the original ruleSet + * + * @var array + */ + public $ruleSet = []; + /** + * Holds the CakeValidationRule objects + * + * @var CakeValidationRule[] + */ + protected $_rules = []; + /** + * List of methods available for validation + * + * @var array + */ + protected $_methods = []; + /** + * I18n domain for validation messages. + * + * @var string + */ + protected $_validationDomain = null; + + /** + * Constructor + * + * @param string $fieldName The fieldname. + * @param array $ruleSet Rules set. + */ + public function __construct($fieldName, $ruleSet) + { + $this->field = $fieldName; + + if (!is_array($ruleSet) || (is_array($ruleSet) && isset($ruleSet['rule']))) { + $ruleSet = [$ruleSet]; + } + + foreach ($ruleSet as $index => $validateProp) { + $this->_rules[$index] = new CakeValidationRule($validateProp); + } + $this->ruleSet = $ruleSet; + } + + /** + * Sets the list of methods to use for validation + * + * @param array &$methods Methods list + * @return void + */ + public function setMethods(&$methods) + { + $this->_methods =& $methods; + } + + /** + * Sets the I18n domain for validation messages. + * + * @param string $validationDomain The validation domain to be used. + * @return void + */ + public function setValidationDomain($validationDomain) + { + $this->_validationDomain = $validationDomain; + } + + /** + * Runs all validation rules in this set and returns a list of + * validation errors + * + * @param array $data Data array + * @param bool $isUpdate Is record being updated or created + * @return array list of validation errors for this field + */ + public function validate($data, $isUpdate = false) + { + $this->reset(); + $errors = []; + foreach ($this->getRules() as $name => $rule) { + $rule->isUpdate($isUpdate); + if ($rule->skip()) { + continue; + } + + $checkRequired = $rule->checkRequired($this->field, $data); + if (!$checkRequired && array_key_exists($this->field, $data)) { + if ($rule->checkEmpty($this->field, $data)) { + break; + } + $rule->process($this->field, $data, $this->_methods); + } + + if ($checkRequired || !$rule->isValid()) { + $errors[] = $this->_processValidationResponse($name, $rule); + if ($rule->isLast()) { + break; + } + } + } + + return $errors; + } + + /** + * Resets internal state for all validation rules in this set + * + * @return void + */ + public function reset() + { + foreach ($this->getRules() as $rule) { + $rule->reset(); + } + } + + /** + * Returns all rules for this validation set + * + * @return CakeValidationRule[] + */ + public function getRules() + { + return $this->_rules; + } + + /** + * Sets the rules for a given field + * + * ## Example: + * + * ``` + * $set->setRules(array( + * 'required' => array('rule' => 'notBlank', 'required' => true), + * 'inRange' => array('rule' => array('between', 4, 10) + * )); + * ``` + * + * @param array $rules The rules to be set + * @param bool $mergeVars [optional] If true, merges vars instead of replace. Defaults to true. + * @return self + */ + public function setRules($rules = [], $mergeVars = true) + { + if ($mergeVars === false) { + $this->_rules = []; + } + foreach ($rules as $name => $rule) { + $this->setRule($name, $rule); + } + return $this; + } + + /** + * Fetches the correct error message for a failed validation + * + * @param string $name the name of the rule as it was configured + * @param CakeValidationRule $rule the object containing validation information + * @return string + */ + protected function _processValidationResponse($name, $rule) + { + $message = $rule->getValidationResult(); + if (is_string($message)) { + return $message; + } + $message = $rule->message; + + if ($message !== null) { + $args = null; + if (is_array($message)) { + $result = $message[0]; + $args = array_slice($message, 1); + } else { + $result = $message; + } + if (is_array($rule->rule) && $args === null) { + $args = array_slice($rule->rule, 1); + } + $args = $this->_translateArgs($args); + + $message = __d($this->_validationDomain, $result, $args); + } else if (is_string($name)) { + if (is_array($rule->rule)) { + $args = array_slice($rule->rule, 1); + $args = $this->_translateArgs($args); + $message = __d($this->_validationDomain, $name, $args); + } else { + $message = __d($this->_validationDomain, $name); + } + } else { + $message = __d('cake', 'This field cannot be left blank'); + } + + return $message; + } + + /** + * Applies translations to validator arguments. + * + * @param array $args The args to translate + * @return array Translated args. + */ + protected function _translateArgs($args) + { + foreach ((array)$args as $k => $arg) { + if (is_string($arg)) { + $args[$k] = __d($this->_validationDomain, $arg); + } + } + return $args; + } + + /** + * Sets a CakeValidationRule $rule with a $name + * + * ## Example: + * + * ``` + * $set + * ->setRule('required', array('rule' => 'notBlank', 'required' => true)) + * ->setRule('between', array('rule' => array('lengthBetween', 4, 10)) + * ``` + * + * @param string $name The name under which the rule should be set + * @param CakeValidationRule|array $rule The validation rule to be set + * @return self + */ + public function setRule($name, $rule) + { + if (!($rule instanceof CakeValidationRule)) { + $rule = new CakeValidationRule($rule); + } + $this->_rules[$name] = $rule; + return $this; + } + + /** + * Gets a rule for a given name if exists + * + * @param string $name Field name. + * @return CakeValidationRule + */ + public function getRule($name) + { + if (!empty($this->_rules[$name])) { + return $this->_rules[$name]; + } + } + + /** + * Removes a validation rule from the set + * + * ## Example: + * + * ``` + * $set + * ->removeRule('required') + * ->removeRule('inRange') + * ``` + * + * @param string $name The name under which the rule should be unset + * @return self + */ + public function removeRule($name) + { + unset($this->_rules[$name]); + return $this; + } + + /** + * Returns whether an index exists in the rule set + * + * @param string $index name of the rule + * @return bool + */ + public function offsetExists($index) + { + return isset($this->_rules[$index]); + } + + /** + * Returns a rule object by its index + * + * @param string $index name of the rule + * @return CakeValidationRule + */ + public function offsetGet($index) + { + return $this->_rules[$index]; + } + + /** + * Sets or replace a validation rule. + * + * This is a wrapper for ArrayAccess. Use setRule() directly for + * chainable access. + * + * @param string $index Name of the rule. + * @param CakeValidationRule|array $rule Rule to add to $index. + * @return void + * @see http://www.php.net/manual/en/arrayobject.offsetset.php + */ + public function offsetSet($index, $rule) + { + $this->setRule($index, $rule); + } + + /** + * Unsets a validation rule + * + * @param string $index name of the rule + * @return void + */ + public function offsetUnset($index) + { + unset($this->_rules[$index]); + } + + /** + * Returns an iterator for each of the rules to be applied + * + * @return ArrayIterator + */ + public function getIterator() + { + return new ArrayIterator($this->_rules); + } + + /** + * Returns the number of rules in this set + * + * @return int + */ + public function count() + { + return count($this->_rules); + } } diff --git a/lib/Cake/Network/CakeRequest.php b/lib/Cake/Network/CakeRequest.php index 93cc3b9f..2ea8e94a 100755 --- a/lib/Cake/Network/CakeRequest.php +++ b/lib/Cake/Network/CakeRequest.php @@ -32,20 +32,21 @@ * @property array $pass Array of passed arguments parsed from the URL. * @package Cake.Network */ -class CakeRequest implements ArrayAccess { +class CakeRequest implements ArrayAccess +{ /** * Array of parameters parsed from the URL. * * @var array */ - public $params = array( + public $params = [ 'plugin' => null, 'controller' => null, 'action' => null, - 'named' => array(), - 'pass' => array(), - ); + 'named' => [], + 'pass' => [], + ]; /** * Array of POST data. Will contain form data as well as uploaded files. @@ -55,14 +56,14 @@ class CakeRequest implements ArrayAccess { * * @var array */ - public $data = array(); + public $data = []; /** * Array of querystring arguments * * @var array */ - public $query = array(); + public $query = []; /** * The URL string used for the request. @@ -100,27 +101,27 @@ class CakeRequest implements ArrayAccess { * * @var array */ - protected $_detectors = array( - 'get' => array('env' => 'REQUEST_METHOD', 'value' => 'GET'), - 'patch' => array('env' => 'REQUEST_METHOD', 'value' => 'PATCH'), - 'post' => array('env' => 'REQUEST_METHOD', 'value' => 'POST'), - 'put' => array('env' => 'REQUEST_METHOD', 'value' => 'PUT'), - 'delete' => array('env' => 'REQUEST_METHOD', 'value' => 'DELETE'), - 'head' => array('env' => 'REQUEST_METHOD', 'value' => 'HEAD'), - 'options' => array('env' => 'REQUEST_METHOD', 'value' => 'OPTIONS'), - 'ssl' => array('env' => 'HTTPS', 'value' => 1), - 'ajax' => array('env' => 'HTTP_X_REQUESTED_WITH', 'value' => 'XMLHttpRequest'), - 'flash' => array('env' => 'HTTP_USER_AGENT', 'pattern' => '/^(Shockwave|Adobe) Flash/'), - 'mobile' => array('env' => 'HTTP_USER_AGENT', 'options' => array( + protected $_detectors = [ + 'get' => ['env' => 'REQUEST_METHOD', 'value' => 'GET'], + 'patch' => ['env' => 'REQUEST_METHOD', 'value' => 'PATCH'], + 'post' => ['env' => 'REQUEST_METHOD', 'value' => 'POST'], + 'put' => ['env' => 'REQUEST_METHOD', 'value' => 'PUT'], + 'delete' => ['env' => 'REQUEST_METHOD', 'value' => 'DELETE'], + 'head' => ['env' => 'REQUEST_METHOD', 'value' => 'HEAD'], + 'options' => ['env' => 'REQUEST_METHOD', 'value' => 'OPTIONS'], + 'ssl' => ['env' => 'HTTPS', 'value' => 1], + 'ajax' => ['env' => 'HTTP_X_REQUESTED_WITH', 'value' => 'XMLHttpRequest'], + 'flash' => ['env' => 'HTTP_USER_AGENT', 'pattern' => '/^(Shockwave|Adobe) Flash/'], + 'mobile' => ['env' => 'HTTP_USER_AGENT', 'options' => [ 'Android', 'AvantGo', 'BB10', 'BlackBerry', 'DoCoMo', 'Fennec', 'iPod', 'iPhone', 'iPad', 'J2ME', 'MIDP', 'NetFront', 'Nokia', 'Opera Mini', 'Opera Mobi', 'PalmOS', 'PalmSource', 'portalmmm', 'Plucker', 'ReqwirelessWeb', 'SonyEricsson', 'Symbian', 'UP\\.Browser', 'webOS', 'Windows CE', 'Windows Phone OS', 'Xiino' - )), - 'requested' => array('param' => 'requested', 'value' => 1), - 'json' => array('accept' => array('application/json'), 'param' => 'ext', 'value' => 'json'), - 'xml' => array('accept' => array('application/xml', 'text/xml'), 'param' => 'ext', 'value' => 'xml'), - ); + ]], + 'requested' => ['param' => 'requested', 'value' => 1], + 'json' => ['accept' => ['application/json'], 'param' => 'ext', 'value' => 'json'], + 'xml' => ['accept' => ['application/xml', 'text/xml'], 'param' => 'ext', 'value' => 'xml'], + ]; /** * Copy of php://input. Since this stream can only be read once in most SAPI's @@ -136,7 +137,8 @@ class CakeRequest implements ArrayAccess { * @param string $url Trimmed URL string to use. Should not contain the application base path. * @param bool $parseEnvironment Set to false to not auto parse the environment. ie. GET, POST and FILES. */ - public function __construct($url = null, $parseEnvironment = true) { + public function __construct($url = null, $parseEnvironment = true) + { $this->_base(); if (empty($url)) { $url = $this->_url(); @@ -155,87 +157,73 @@ public function __construct($url = null, $parseEnvironment = true) { } /** - * process the post data and set what is there into the object. - * processed data is available at `$this->data` - * - * Will merge POST vars prefixed with `data`, and ones without - * into a single array. Variables prefixed with `data` will overwrite those without. + * Returns a base URL and sets the proper webroot * - * If you have mixed POST values be careful not to make any top level keys numeric - * containing arrays. Hash::merge() is used to merge data, and it has possibly - * unexpected behavior in this situation. + * If CakePHP is called with index.php in the URL even though + * URL Rewriting is activated (and thus not needed) it swallows + * the unnecessary part from $base to prevent issue #3318. * - * @return void + * @return string Base URL */ - protected function _processPost() { - if ($_POST) { - $this->data = $_POST; - } elseif (($this->is('put') || $this->is('delete')) && - strpos($this->contentType(), 'application/x-www-form-urlencoded') === 0 - ) { - $data = $this->_readInput(); - parse_str($data, $this->data); + protected function _base() + { + $dir = $webroot = null; + $config = Configure::read('App'); + extract($config); + + if (!isset($base)) { + $base = $this->base; } - if (ini_get('magic_quotes_gpc') === '1') { - $this->data = stripslashes_deep($this->data); + if ($base !== false) { + $this->webroot = $base . '/'; + return $this->base = $base; } - $override = null; - if (env('HTTP_X_HTTP_METHOD_OVERRIDE')) { - $this->data['_method'] = env('HTTP_X_HTTP_METHOD_OVERRIDE'); - $override = $this->data['_method']; - } + if (empty($baseUrl)) { + $base = dirname(env('PHP_SELF')); + // Clean up additional / which cause following code to fail.. + $base = preg_replace('#/+#', '/', $base); - $isArray = is_array($this->data); - if ($isArray && isset($this->data['_method'])) { - if (!empty($_SERVER)) { - $_SERVER['REQUEST_METHOD'] = $this->data['_method']; - } else { - $_ENV['REQUEST_METHOD'] = $this->data['_method']; + $indexPos = strpos($base, '/webroot/index.php'); + if ($indexPos !== false) { + $base = substr($base, 0, $indexPos) . '/webroot'; + } + if ($webroot === 'webroot' && $webroot === basename($base)) { + $base = dirname($base); + } + if ($dir === 'app' && $dir === basename($base)) { + $base = dirname($base); } - $override = $this->data['_method']; - unset($this->data['_method']); - } - - if ($override && !in_array($override, array('POST', 'PUT', 'PATCH', 'DELETE'))) { - $this->data = array(); - } - if ($isArray && isset($this->data['data'])) { - $data = $this->data['data']; - if (count($this->data) <= 1) { - $this->data = $data; - } else { - unset($this->data['data']); - $this->data = Hash::merge($this->data, $data); + if ($base === DS || $base === '.') { + $base = ''; } - } - } + $base = implode('/', array_map('rawurlencode', explode('/', $base))); + $this->webroot = $base . '/'; - /** - * Process the GET parameters and move things into the object. - * - * @return void - */ - protected function _processGet() { - if (ini_get('magic_quotes_gpc') === '1') { - $query = stripslashes_deep($_GET); - } else { - $query = $_GET; + return $this->base = $base; } - $unsetUrl = '/' . str_replace(array('.', ' '), '_', urldecode($this->url)); - unset($query[$unsetUrl]); - unset($query[$this->base . $unsetUrl]); - if (strpos($this->url, '?') !== false) { - list($this->url, $querystr) = explode('?', $this->url); - parse_str($querystr, $queryArgs); - $query += $queryArgs; + $file = '/' . basename($baseUrl); + $base = dirname($baseUrl); + + if ($base === DS || $base === '.') { + $base = ''; } - if (isset($this->params['url'])) { - $query = array_merge($this->params['url'], $query); + $this->webroot = $base . '/'; + + $docRoot = env('DOCUMENT_ROOT'); + $docRootContainsWebroot = strpos($docRoot, $dir . DS . $webroot); + + if (!empty($base) || !$docRootContainsWebroot) { + if (strpos($this->webroot, '/' . $dir . '/') === false) { + $this->webroot .= $dir . '/'; + } + if (strpos($this->webroot, '/' . $webroot . '/') === false) { + $this->webroot .= $webroot . '/'; + } } - $this->query = $query; + return $this->base = $base . $file; } /** @@ -245,13 +233,14 @@ protected function _processGet() { * * @return string URI The CakePHP request path that is being accessed. */ - protected function _url() { + protected function _url() + { $uri = ''; if (!empty($_SERVER['PATH_INFO'])) { return $_SERVER['PATH_INFO']; - } elseif (isset($_SERVER['REQUEST_URI']) && strpos($_SERVER['REQUEST_URI'], '://') === false) { + } else if (isset($_SERVER['REQUEST_URI']) && strpos($_SERVER['REQUEST_URI'], '://') === false) { $uri = $_SERVER['REQUEST_URI']; - } elseif (isset($_SERVER['REQUEST_URI'])) { + } else if (isset($_SERVER['REQUEST_URI'])) { $qPosition = strpos($_SERVER['REQUEST_URI'], '?'); if ($qPosition !== false && strpos($_SERVER['REQUEST_URI'], '://') > $qPosition) { $uri = $_SERVER['REQUEST_URI']; @@ -261,11 +250,11 @@ protected function _url() { $uri = substr($_SERVER['REQUEST_URI'], strlen($baseUrl)); } } - } elseif (isset($_SERVER['PHP_SELF']) && isset($_SERVER['SCRIPT_NAME'])) { + } else if (isset($_SERVER['PHP_SELF']) && isset($_SERVER['SCRIPT_NAME'])) { $uri = str_replace($_SERVER['SCRIPT_NAME'], '', $_SERVER['PHP_SELF']); - } elseif (isset($_SERVER['HTTP_X_REWRITE_URL'])) { + } else if (isset($_SERVER['HTTP_X_REWRITE_URL'])) { $uri = $_SERVER['HTTP_X_REWRITE_URL']; - } elseif ($var = env('argv')) { + } else if ($var = env('argv')) { $uri = $var[0]; } @@ -291,117 +280,185 @@ protected function _url() { } /** - * Returns a base URL and sets the proper webroot + * process the post data and set what is there into the object. + * processed data is available at `$this->data` * - * If CakePHP is called with index.php in the URL even though - * URL Rewriting is activated (and thus not needed) it swallows - * the unnecessary part from $base to prevent issue #3318. + * Will merge POST vars prefixed with `data`, and ones without + * into a single array. Variables prefixed with `data` will overwrite those without. * - * @return string Base URL + * If you have mixed POST values be careful not to make any top level keys numeric + * containing arrays. Hash::merge() is used to merge data, and it has possibly + * unexpected behavior in this situation. + * + * @return void */ - protected function _base() { - $dir = $webroot = null; - $config = Configure::read('App'); - extract($config); - - if (!isset($base)) { - $base = $this->base; + protected function _processPost() + { + if ($_POST) { + $this->data = $_POST; + } else if (($this->is('put') || $this->is('delete')) && + strpos($this->contentType(), 'application/x-www-form-urlencoded') === 0 + ) { + $data = $this->_readInput(); + parse_str($data, $this->data); } - if ($base !== false) { - $this->webroot = $base . '/'; - return $this->base = $base; + if (ini_get('magic_quotes_gpc') === '1') { + $this->data = stripslashes_deep($this->data); } - if (empty($baseUrl)) { - $base = dirname(env('PHP_SELF')); - // Clean up additional / which cause following code to fail.. - $base = preg_replace('#/+#', '/', $base); - - $indexPos = strpos($base, '/webroot/index.php'); - if ($indexPos !== false) { - $base = substr($base, 0, $indexPos) . '/webroot'; - } - if ($webroot === 'webroot' && $webroot === basename($base)) { - $base = dirname($base); - } - if ($dir === 'app' && $dir === basename($base)) { - $base = dirname($base); - } + $override = null; + if (env('HTTP_X_HTTP_METHOD_OVERRIDE')) { + $this->data['_method'] = env('HTTP_X_HTTP_METHOD_OVERRIDE'); + $override = $this->data['_method']; + } - if ($base === DS || $base === '.') { - $base = ''; + $isArray = is_array($this->data); + if ($isArray && isset($this->data['_method'])) { + if (!empty($_SERVER)) { + $_SERVER['REQUEST_METHOD'] = $this->data['_method']; + } else { + $_ENV['REQUEST_METHOD'] = $this->data['_method']; } - $base = implode('/', array_map('rawurlencode', explode('/', $base))); - $this->webroot = $base . '/'; - - return $this->base = $base; + $override = $this->data['_method']; + unset($this->data['_method']); } - $file = '/' . basename($baseUrl); - $base = dirname($baseUrl); + if ($override && !in_array($override, ['POST', 'PUT', 'PATCH', 'DELETE'])) { + $this->data = []; + } - if ($base === DS || $base === '.') { - $base = ''; + if ($isArray && isset($this->data['data'])) { + $data = $this->data['data']; + if (count($this->data) <= 1) { + $this->data = $data; + } else { + unset($this->data['data']); + $this->data = Hash::merge($this->data, $data); + } } - $this->webroot = $base . '/'; + } - $docRoot = env('DOCUMENT_ROOT'); - $docRootContainsWebroot = strpos($docRoot, $dir . DS . $webroot); + /** + * Check whether or not a Request is a certain type. + * + * Uses the built in detection rules as well as additional rules + * defined with CakeRequest::addDetector(). Any detector can be called + * as `is($type)` or `is$Type()`. + * + * @param string|string[] $type The type of request you want to check. If an array + * this method will return true if the request matches any type. + * @return bool Whether or not the request is the type you are checking. + */ + public function is($type) + { + if (is_array($type)) { + foreach ($type as $_type) { + if ($this->is($_type)) { + return true; + } + } + return false; + } + $type = strtolower($type); + if (!isset($this->_detectors[$type])) { + return false; + } + $detect = $this->_detectors[$type]; + if (isset($detect['env']) && $this->_environmentDetector($detect)) { + return true; + } + if (isset($detect['header']) && $this->_headerDetector($detect)) { + return true; + } + if (isset($detect['accept']) && $this->_acceptHeaderDetector($detect)) { + return true; + } + if (isset($detect['param']) && $this->_paramDetector($detect)) { + return true; + } + if (isset($detect['callback']) && is_callable($detect['callback'])) { + return call_user_func($detect['callback'], $this); + } + return false; + } - if (!empty($base) || !$docRootContainsWebroot) { - if (strpos($this->webroot, '/' . $dir . '/') === false) { - $this->webroot .= $dir . '/'; + /** + * Detects if a specific environment variable is present. + * + * @param array $detect Detector options array. + * @return bool Whether or not the request is the type you are checking. + */ + protected function _environmentDetector($detect) + { + if (isset($detect['env'])) { + if (isset($detect['value'])) { + return env($detect['env']) == $detect['value']; } - if (strpos($this->webroot, '/' . $webroot . '/') === false) { - $this->webroot .= $webroot . '/'; + if (isset($detect['pattern'])) { + return (bool)preg_match($detect['pattern'], env($detect['env'])); + } + if (isset($detect['options'])) { + $pattern = '/' . implode('|', $detect['options']) . '/i'; + return (bool)preg_match($pattern, env($detect['env'])); } } - return $this->base = $base . $file; + return false; } /** - * Process $_FILES and move things into the object. + * Detects if a specific header is present. * - * @return void + * @param array $detect Detector options array. + * @return bool Whether or not the request is the type you are checking. */ - protected function _processFiles() { - if (isset($_FILES) && is_array($_FILES)) { - foreach ($_FILES as $name => $data) { - if ($name !== 'data') { - $this->params['form'][$name] = $data; + protected function _headerDetector($detect) + { + foreach ($detect['header'] as $header => $value) { + $header = env('HTTP_' . strtoupper($header)); + if (!is_null($header)) { + if (!is_string($value) && !is_bool($value) && is_callable($value)) { + return call_user_func($value, $header); } + return ($header === $value); } } + return false; + } - if (isset($_FILES['data'])) { - foreach ($_FILES['data'] as $key => $data) { - $this->_processFileData('', $data, $key); + /** + * Detects if a specific accept header is present. + * + * @param array $detect Detector options array. + * @return bool Whether or not the request is the type you are checking. + */ + protected function _acceptHeaderDetector($detect) + { + $acceptHeaders = explode(',', (string)env('HTTP_ACCEPT')); + foreach ($detect['accept'] as $header) { + if (in_array($header, $acceptHeaders)) { + return true; } } + return false; } /** - * Recursively walks the FILES array restructuring the data - * into something sane and useable. + * Detects if a specific request parameter is present. * - * @param string $path The dot separated path to insert $data into. - * @param array $data The data to traverse/insert. - * @param string $field The terminal field name, which is the top level key in $_FILES. - * @return void + * @param array $detect Detector options array. + * @return bool Whether or not the request is the type you are checking. */ - protected function _processFileData($path, $data, $field) { - foreach ($data as $key => $fields) { - $newPath = $key; - if (strlen($path) > 0) { - $newPath = $path . '.' . $key; - } - if (is_array($fields)) { - $this->_processFileData($newPath, $fields, $field); - } else { - $newPath .= '.' . $field; - $this->data = Hash::insert($this->data, $newPath, $fields); - } + protected function _paramDetector($detect) + { + $key = $detect['param']; + if (isset($detect['value'])) { + $value = $detect['value']; + return isset($this->params[$key]) ? $this->params[$key] == $value : false; } + if (isset($detect['options'])) { + return isset($this->params[$key]) ? in_array($this->params[$key], $detect['options']) : false; + } + return false; } /** @@ -409,7 +466,8 @@ protected function _processFileData($path, $data, $field) { * * @return string */ - public function contentType() { + public function contentType() + { $type = env('CONTENT_TYPE'); if ($type) { return $type; @@ -418,224 +476,218 @@ public function contentType() { } /** - * Get the IP the client is using, or says they are using. + * Read data from php://input, mocked in tests. * - * @param bool $safe Use safe = false when you think the user might manipulate their HTTP_CLIENT_IP - * header. Setting $safe = false will also look at HTTP_X_FORWARDED_FOR - * @return string The client IP. + * @return string contents of php://input */ - public function clientIp($safe = true) { - if (!$safe && env('HTTP_X_FORWARDED_FOR')) { - $ipaddr = preg_replace('/(?:,.*)/', '', env('HTTP_X_FORWARDED_FOR')); - } elseif (!$safe && env('HTTP_CLIENT_IP')) { - $ipaddr = env('HTTP_CLIENT_IP'); - } else { - $ipaddr = env('REMOTE_ADDR'); + protected function _readInput() + { + if (empty($this->_input)) { + $fh = fopen('php://input', 'r'); + $content = stream_get_contents($fh); + fclose($fh); + $this->_input = $content; } - return trim($ipaddr); + return $this->_input; } /** - * Returns the referer that referred this request. + * Process the GET parameters and move things into the object. * - * @param bool $local Attempt to return a local address. Local addresses do not contain hostnames. - * @return string The referring address for this request. + * @return void */ - public function referer($local = false) { - $ref = env('HTTP_REFERER'); + protected function _processGet() + { + if (ini_get('magic_quotes_gpc') === '1') { + $query = stripslashes_deep($_GET); + } else { + $query = $_GET; + } - $base = Configure::read('App.fullBaseUrl') . $this->webroot; - if (!empty($ref) && !empty($base)) { - if ($local && strpos($ref, $base) === 0) { - $ref = substr($ref, strlen($base)); - if (!strlen($ref) || strpos($ref, '//') === 0) { - $ref = '/'; - } - if ($ref[0] !== '/') { - $ref = '/' . $ref; - } - return $ref; - } elseif (!$local) { - return $ref; - } + $unsetUrl = '/' . str_replace(['.', ' '], '_', urldecode($this->url)); + unset($query[$unsetUrl]); + unset($query[$this->base . $unsetUrl]); + if (strpos($this->url, '?') !== false) { + list($this->url, $querystr) = explode('?', $this->url); + parse_str($querystr, $queryArgs); + $query += $queryArgs; } - return '/'; + if (isset($this->params['url'])) { + $query = array_merge($this->params['url'], $query); + } + $this->query = $query; } /** - * Missing method handler, handles wrapping older style isAjax() type methods + * Process $_FILES and move things into the object. * - * @param string $name The method called - * @param array $params Array of parameters for the method call - * @return mixed - * @throws CakeException when an invalid method is called. + * @return void */ - public function __call($name, $params) { - if (strpos($name, 'is') === 0) { - $type = strtolower(substr($name, 2)); - return $this->is($type); + protected function _processFiles() + { + if (isset($_FILES) && is_array($_FILES)) { + foreach ($_FILES as $name => $data) { + if ($name !== 'data') { + $this->params['form'][$name] = $data; + } + } } - throw new CakeException(__d('cake_dev', 'Method %s does not exist', $name)); - } - /** - * Magic get method allows access to parsed routing parameters directly on the object. - * - * Allows access to `$this->params['controller']` via `$this->controller` - * - * @param string $name The property being accessed. - * @return mixed Either the value of the parameter or null. - */ - public function __get($name) { - if (isset($this->params[$name])) { - return $this->params[$name]; + if (isset($_FILES['data'])) { + foreach ($_FILES['data'] as $key => $data) { + $this->_processFileData('', $data, $key); + } } - return null; } /** - * Magic isset method allows isset/empty checks - * on routing parameters. + * Recursively walks the FILES array restructuring the data + * into something sane and useable. * - * @param string $name The property being accessed. - * @return bool Existence + * @param string $path The dot separated path to insert $data into. + * @param array $data The data to traverse/insert. + * @param string $field The terminal field name, which is the top level key in $_FILES. + * @return void */ - public function __isset($name) { - return isset($this->params[$name]); + protected function _processFileData($path, $data, $field) + { + foreach ($data as $key => $fields) { + $newPath = $key; + if (strlen($path) > 0) { + $newPath = $path . '.' . $key; + } + if (is_array($fields)) { + $this->_processFileData($newPath, $fields, $field); + } else { + $newPath .= '.' . $field; + $this->data = Hash::insert($this->data, $newPath, $fields); + } + } } /** - * Check whether or not a Request is a certain type. + * Get the languages accepted by the client, or check if a specific language is accepted. * - * Uses the built in detection rules as well as additional rules - * defined with CakeRequest::addDetector(). Any detector can be called - * as `is($type)` or `is$Type()`. + * Get the list of accepted languages: * - * @param string|string[] $type The type of request you want to check. If an array - * this method will return true if the request matches any type. - * @return bool Whether or not the request is the type you are checking. + * ``` CakeRequest::acceptLanguage(); ``` + * + * Check if a specific language is accepted: + * + * ``` CakeRequest::acceptLanguage('es-es'); ``` + * + * @param string $language The language to test. + * @return mixed If a $language is provided, a boolean. Otherwise the array of accepted languages. */ - public function is($type) { - if (is_array($type)) { - foreach ($type as $_type) { - if ($this->is($_type)) { - return true; + public static function acceptLanguage($language = null) + { + $raw = static::_parseAcceptWithQualifier(static::header('Accept-Language')); + $accept = []; + foreach ($raw as $languages) { + foreach ($languages as &$lang) { + if (strpos($lang, '_')) { + $lang = str_replace('_', '-', $lang); } + $lang = strtolower($lang); } - return false; - } - $type = strtolower($type); - if (!isset($this->_detectors[$type])) { - return false; - } - $detect = $this->_detectors[$type]; - if (isset($detect['env']) && $this->_environmentDetector($detect)) { - return true; - } - if (isset($detect['header']) && $this->_headerDetector($detect)) { - return true; - } - if (isset($detect['accept']) && $this->_acceptHeaderDetector($detect)) { - return true; - } - if (isset($detect['param']) && $this->_paramDetector($detect)) { - return true; + $accept = array_merge($accept, $languages); } - if (isset($detect['callback']) && is_callable($detect['callback'])) { - return call_user_func($detect['callback'], $this); + if ($language === null) { + return $accept; } - return false; + return in_array(strtolower($language), $accept); } /** - * Detects if a URL extension is present. + * Get the IP the client is using, or says they are using. * - * @param array $detect Detector options array. - * @return bool Whether or not the request is the type you are checking. + * @param bool $safe Use safe = false when you think the user might manipulate their HTTP_CLIENT_IP + * header. Setting $safe = false will also look at HTTP_X_FORWARDED_FOR + * @return string The client IP. */ - protected function _extensionDetector($detect) { - if (is_string($detect['extension'])) { - $detect['extension'] = array($detect['extension']); - } - if (in_array($this->params['ext'], $detect['extension'])) { - return true; + public function clientIp($safe = true) + { + if (!$safe && env('HTTP_X_FORWARDED_FOR')) { + $ipaddr = preg_replace('/(?:,.*)/', '', env('HTTP_X_FORWARDED_FOR')); + } else if (!$safe && env('HTTP_CLIENT_IP')) { + $ipaddr = env('HTTP_CLIENT_IP'); + } else { + $ipaddr = env('REMOTE_ADDR'); } - return false; + return trim($ipaddr); } /** - * Detects if a specific accept header is present. + * Returns the referer that referred this request. * - * @param array $detect Detector options array. - * @return bool Whether or not the request is the type you are checking. + * @param bool $local Attempt to return a local address. Local addresses do not contain hostnames. + * @return string The referring address for this request. */ - protected function _acceptHeaderDetector($detect) { - $acceptHeaders = explode(',', (string)env('HTTP_ACCEPT')); - foreach ($detect['accept'] as $header) { - if (in_array($header, $acceptHeaders)) { - return true; + public function referer($local = false) + { + $ref = env('HTTP_REFERER'); + + $base = Configure::read('App.fullBaseUrl') . $this->webroot; + if (!empty($ref) && !empty($base)) { + if ($local && strpos($ref, $base) === 0) { + $ref = substr($ref, strlen($base)); + if (!strlen($ref) || strpos($ref, '//') === 0) { + $ref = '/'; + } + if ($ref[0] !== '/') { + $ref = '/' . $ref; + } + return $ref; + } else if (!$local) { + return $ref; } } - return false; + return '/'; } /** - * Detects if a specific header is present. + * Missing method handler, handles wrapping older style isAjax() type methods * - * @param array $detect Detector options array. - * @return bool Whether or not the request is the type you are checking. + * @param string $name The method called + * @param array $params Array of parameters for the method call + * @return mixed + * @throws CakeException when an invalid method is called. */ - protected function _headerDetector($detect) { - foreach ($detect['header'] as $header => $value) { - $header = env('HTTP_' . strtoupper($header)); - if (!is_null($header)) { - if (!is_string($value) && !is_bool($value) && is_callable($value)) { - return call_user_func($value, $header); - } - return ($header === $value); - } + public function __call($name, $params) + { + if (strpos($name, 'is') === 0) { + $type = strtolower(substr($name, 2)); + return $this->is($type); } - return false; + throw new CakeException(__d('cake_dev', 'Method %s does not exist', $name)); } /** - * Detects if a specific request parameter is present. + * Magic get method allows access to parsed routing parameters directly on the object. * - * @param array $detect Detector options array. - * @return bool Whether or not the request is the type you are checking. + * Allows access to `$this->params['controller']` via `$this->controller` + * + * @param string $name The property being accessed. + * @return mixed Either the value of the parameter or null. */ - protected function _paramDetector($detect) { - $key = $detect['param']; - if (isset($detect['value'])) { - $value = $detect['value']; - return isset($this->params[$key]) ? $this->params[$key] == $value : false; - } - if (isset($detect['options'])) { - return isset($this->params[$key]) ? in_array($this->params[$key], $detect['options']) : false; + public function __get($name) + { + if (isset($this->params[$name])) { + return $this->params[$name]; } - return false; + return null; } /** - * Detects if a specific environment variable is present. + * Magic isset method allows isset/empty checks + * on routing parameters. * - * @param array $detect Detector options array. - * @return bool Whether or not the request is the type you are checking. + * @param string $name The property being accessed. + * @return bool Existence */ - protected function _environmentDetector($detect) { - if (isset($detect['env'])) { - if (isset($detect['value'])) { - return env($detect['env']) == $detect['value']; - } - if (isset($detect['pattern'])) { - return (bool)preg_match($detect['pattern'], env($detect['env'])); - } - if (isset($detect['options'])) { - $pattern = '/' . implode('|', $detect['options']) . '/i'; - return (bool)preg_match($pattern, env($detect['env'])); - } - } - return false; + public function __isset($name) + { + return isset($this->params[$name]); } /** @@ -649,7 +701,8 @@ protected function _environmentDetector($detect) { * @return bool Success. * @see CakeRequest::is() */ - public function isAll(array $types) { + public function isAll(array $types) + { foreach ($types as $type) { if (!$this->is($type)) { return false; @@ -705,7 +758,8 @@ public function isAll(array $types) { * @param array $options The options for the detector definition. See above. * @return void */ - public function addDetector($name, $options) { + public function addDetector($name, $options) + { $name = strtolower($name); if (isset($this->_detectors[$name]) && isset($options['options'])) { $options = Hash::merge($this->_detectors[$name], $options); @@ -720,7 +774,8 @@ public function addDetector($name, $options) { * @param array $params Array of parameters to merge in * @return self */ - public function addParams($params) { + public function addParams($params) + { $this->params = array_merge($this->params, (array)$params); return $this; } @@ -732,8 +787,9 @@ public function addParams($params) { * @param array $paths Array of paths to merge in * @return self */ - public function addPaths($paths) { - foreach (array('webroot', 'here', 'base') as $element) { + public function addPaths($paths) + { + foreach (['webroot', 'here', 'base'] as $element) { if (isset($paths[$element])) { $this->{$element} = $paths[$element]; } @@ -747,7 +803,8 @@ public function addPaths($paths) { * @param bool $base Include the base path, set to false to trim the base path off. * @return string the current request URL including query string args. */ - public function here($base = true) { + public function here($base = true) + { $url = $this->here; if (!empty($this->query)) { $url .= '?' . http_build_query($this->query, null, '&'); @@ -758,25 +815,6 @@ public function here($base = true) { return $url; } - /** - * Read an HTTP header from the Request information. - * - * @param string $name Name of the header you want. - * @return mixed Either false on no header being set or the value of the header. - */ - public static function header($name) { - $httpName = 'HTTP_' . strtoupper(str_replace('-', '_', $name)); - if (isset($_SERVER[$httpName])) { - return $_SERVER[$httpName]; - } - // Use the provided value, in some configurations apache will - // pass Authorization with no prefix and in Titlecase. - if (isset($_SERVER[$name])) { - return $_SERVER[$name]; - } - return false; - } - /** * Get the HTTP method used for this request. * There are a few ways to specify a method. @@ -790,23 +828,11 @@ public static function header($name) { * * @return string The name of the HTTP method used. */ - public function method() { + public function method() + { return env('REQUEST_METHOD'); } - /** - * Get the host that the request was handled on. - * - * @param bool $trustProxy Whether or not to trust the proxy host. - * @return string - */ - public function host($trustProxy = false) { - if ($trustProxy) { - return env('HTTP_X_FORWARDED_HOST'); - } - return env('HTTP_HOST'); - } - /** * Get the domain name and include $tldLength segments of the tld. * @@ -814,12 +840,27 @@ public function host($trustProxy = false) { * While `example.co.uk` contains 2. * @return string Domain name without subdomains. */ - public function domain($tldLength = 1) { + public function domain($tldLength = 1) + { $segments = explode('.', $this->host()); $domain = array_slice($segments, -1 * ($tldLength + 1)); return implode('.', $domain); } + /** + * Get the host that the request was handled on. + * + * @param bool $trustProxy Whether or not to trust the proxy host. + * @return string + */ + public function host($trustProxy = false) + { + if ($trustProxy) { + return env('HTTP_X_FORWARDED_HOST'); + } + return env('HTTP_HOST'); + } + /** * Get the subdomains for a host. * @@ -827,7 +868,8 @@ public function domain($tldLength = 1) { * While `example.co.uk` contains 2. * @return array An array of subdomains. */ - public function subdomains($tldLength = 1) { + public function subdomains($tldLength = 1) + { $segments = explode('.', $this->host()); return array_slice($segments, 0, -1 * ($tldLength + 1)); } @@ -851,9 +893,10 @@ public function subdomains($tldLength = 1) { * @return mixed Either an array of all the types the client accepts or a boolean if they accept the * provided type. */ - public function accepts($type = null) { + public function accepts($type = null) + { $raw = $this->parseAccept(); - $accept = array(); + $accept = []; foreach ($raw as $types) { $accept = array_merge($accept, $types); } @@ -872,42 +915,11 @@ public function accepts($type = null) { * * @return array An array of prefValue => array(content/types) */ - public function parseAccept() { + public function parseAccept() + { return $this->_parseAcceptWithQualifier($this->header('accept')); } - /** - * Get the languages accepted by the client, or check if a specific language is accepted. - * - * Get the list of accepted languages: - * - * ``` CakeRequest::acceptLanguage(); ``` - * - * Check if a specific language is accepted: - * - * ``` CakeRequest::acceptLanguage('es-es'); ``` - * - * @param string $language The language to test. - * @return mixed If a $language is provided, a boolean. Otherwise the array of accepted languages. - */ - public static function acceptLanguage($language = null) { - $raw = static::_parseAcceptWithQualifier(static::header('Accept-Language')); - $accept = array(); - foreach ($raw as $languages) { - foreach ($languages as &$lang) { - if (strpos($lang, '_')) { - $lang = str_replace('_', '-', $lang); - } - $lang = strtolower($lang); - } - $accept = array_merge($accept, $languages); - } - if ($language === null) { - return $accept; - } - return in_array(strtolower($language), $accept); - } - /** * Parse Accept* headers with qualifier options. * @@ -917,8 +929,9 @@ public static function acceptLanguage($language = null) { * @param string $header Header to parse. * @return array */ - protected static function _parseAcceptWithQualifier($header) { - $accept = array(); + protected static function _parseAcceptWithQualifier($header) + { + $accept = []; $header = explode(',', $header); foreach (array_filter($header) as $value) { $prefValue = '1.0'; @@ -937,7 +950,7 @@ protected static function _parseAcceptWithQualifier($header) { } if (!isset($accept[$prefValue])) { - $accept[$prefValue] = array(); + $accept[$prefValue] = []; } if ($prefValue) { $accept[$prefValue][] = $value; @@ -947,6 +960,26 @@ protected static function _parseAcceptWithQualifier($header) { return $accept; } + /** + * Read an HTTP header from the Request information. + * + * @param string $name Name of the header you want. + * @return mixed Either false on no header being set or the value of the header. + */ + public static function header($name) + { + $httpName = 'HTTP_' . strtoupper(str_replace('-', '_', $name)); + if (isset($_SERVER[$httpName])) { + return $_SERVER[$httpName]; + } + // Use the provided value, in some configurations apache will + // pass Authorization with no prefix and in Titlecase. + if (isset($_SERVER[$name])) { + return $_SERVER[$name]; + } + return false; + } + /** * Provides a read accessor for `$this->query`. Allows you * to use a syntax similar to `CakeSession` for reading URL query data. @@ -954,7 +987,8 @@ protected static function _parseAcceptWithQualifier($header) { * @param string $name Query string variable name * @return mixed The value being read */ - public function query($name) { + public function query($name) + { return Hash::get($this->query, $name); } @@ -978,7 +1012,8 @@ public function query($name) { * @param string $name Dot separated name of the value to read/write, one or more args. * @return mixed|self Either the value being read, or $this so you can chain consecutive writes. */ - public function data($name) { + public function data($name) + { $args = func_get_args(); if (count($args) === 2) { $this->data = Hash::insert($this->data, $name, $args[1]); @@ -994,7 +1029,8 @@ public function data($name) { * @return mixed The value of the provided parameter. Will * return false if the parameter doesn't exist or is falsey. */ - public function param($name) { + public function param($name) + { $args = func_get_args(); if (count($args) === 2) { $this->params = Hash::insert($this->params, $name, $args[1]); @@ -1025,7 +1061,8 @@ public function param($name) { * supply additional parameters for the decoding callback using var args, see above. * @return mixed The decoded/processed request data. */ - public function input($callback = null) { + public function input($callback = null) + { $input = $this->_readInput(); $args = func_get_args(); if (!empty($args)) { @@ -1043,10 +1080,28 @@ public function input($callback = null) { * @param string $input A string to replace original parsed data from input() * @return void */ - public function setInput($input) { + public function setInput($input) + { $this->_input = $input; } + /** + * Alias of CakeRequest::allowMethod() for backwards compatibility. + * + * @param string|array $methods Allowed HTTP request methods. + * @return bool true + * @throws MethodNotAllowedException + * @see CakeRequest::allowMethod() + * @deprecated 3.0.0 Since 2.5, use CakeRequest::allowMethod() instead. + */ + public function onlyAllow($methods) + { + if (!is_array($methods)) { + $methods = func_get_args(); + } + return $this->allowMethod($methods); + } + /** * Allow only certain HTTP request methods. If the request method does not match * a 405 error will be shown and the required "Allow" response header will be set. @@ -1064,7 +1119,8 @@ public function setInput($input) { * @return bool true * @throws MethodNotAllowedException */ - public function allowMethod($methods) { + public function allowMethod($methods) + { if (!is_array($methods)) { $methods = func_get_args(); } @@ -1079,44 +1135,14 @@ public function allowMethod($methods) { throw $e; } - /** - * Alias of CakeRequest::allowMethod() for backwards compatibility. - * - * @param string|array $methods Allowed HTTP request methods. - * @return bool true - * @throws MethodNotAllowedException - * @see CakeRequest::allowMethod() - * @deprecated 3.0.0 Since 2.5, use CakeRequest::allowMethod() instead. - */ - public function onlyAllow($methods) { - if (!is_array($methods)) { - $methods = func_get_args(); - } - return $this->allowMethod($methods); - } - - /** - * Read data from php://input, mocked in tests. - * - * @return string contents of php://input - */ - protected function _readInput() { - if (empty($this->_input)) { - $fh = fopen('php://input', 'r'); - $content = stream_get_contents($fh); - fclose($fh); - $this->_input = $content; - } - return $this->_input; - } - /** * Array access read implementation * * @param string $name Name of the key being accessed. * @return mixed */ - public function offsetGet($name) { + public function offsetGet($name) + { if (isset($this->params[$name])) { return $this->params[$name]; } @@ -1136,7 +1162,8 @@ public function offsetGet($name) { * @param mixed $value The value being written. * @return void */ - public function offsetSet($name, $value) { + public function offsetSet($name, $value) + { $this->params[$name] = $value; } @@ -1146,7 +1173,8 @@ public function offsetSet($name, $value) { * @param string $name thing to check. * @return bool */ - public function offsetExists($name) { + public function offsetExists($name) + { if ($name === 'url' || $name === 'data') { return true; } @@ -1159,8 +1187,26 @@ public function offsetExists($name) { * @param string $name Name to unset. * @return void */ - public function offsetUnset($name) { + public function offsetUnset($name) + { unset($this->params[$name]); } + /** + * Detects if a URL extension is present. + * + * @param array $detect Detector options array. + * @return bool Whether or not the request is the type you are checking. + */ + protected function _extensionDetector($detect) + { + if (is_string($detect['extension'])) { + $detect['extension'] = [$detect['extension']]; + } + if (in_array($this->params['ext'], $detect['extension'])) { + return true; + } + return false; + } + } \ No newline at end of file diff --git a/lib/Cake/Network/CakeResponse.php b/lib/Cake/Network/CakeResponse.php index cd24b2aa..09cddc78 100755 --- a/lib/Cake/Network/CakeResponse.php +++ b/lib/Cake/Network/CakeResponse.php @@ -26,1509 +26,1556 @@ * * @package Cake.Network */ -class CakeResponse { - -/** - * Holds HTTP response statuses - * - * @var array - */ - protected $_statusCodes = array( - 100 => 'Continue', - 101 => 'Switching Protocols', - 200 => 'OK', - 201 => 'Created', - 202 => 'Accepted', - 203 => 'Non-Authoritative Information', - 204 => 'No Content', - 205 => 'Reset Content', - 206 => 'Partial Content', - 300 => 'Multiple Choices', - 301 => 'Moved Permanently', - 302 => 'Found', - 303 => 'See Other', - 304 => 'Not Modified', - 305 => 'Use Proxy', - 307 => 'Temporary Redirect', - 400 => 'Bad Request', - 401 => 'Unauthorized', - 402 => 'Payment Required', - 403 => 'Forbidden', - 404 => 'Not Found', - 405 => 'Method Not Allowed', - 406 => 'Not Acceptable', - 407 => 'Proxy Authentication Required', - 408 => 'Request Time-out', - 409 => 'Conflict', - 410 => 'Gone', - 411 => 'Length Required', - 412 => 'Precondition Failed', - 413 => 'Request Entity Too Large', - 414 => 'Request-URI Too Large', - 415 => 'Unsupported Media Type', - 416 => 'Requested range not satisfiable', - 417 => 'Expectation Failed', - 429 => 'Too Many Requests', - 500 => 'Internal Server Error', - 501 => 'Not Implemented', - 502 => 'Bad Gateway', - 503 => 'Service Unavailable', - 504 => 'Gateway Time-out', - 505 => 'Unsupported Version' - ); - -/** - * Holds known mime type mappings - * - * @var array - */ - protected $_mimeTypes = array( - 'html' => array('text/html', '*/*'), - 'json' => 'application/json', - 'xml' => array('application/xml', 'text/xml'), - 'rss' => 'application/rss+xml', - 'ai' => 'application/postscript', - 'bcpio' => 'application/x-bcpio', - 'bin' => 'application/octet-stream', - 'ccad' => 'application/clariscad', - 'cdf' => 'application/x-netcdf', - 'class' => 'application/octet-stream', - 'cpio' => 'application/x-cpio', - 'cpt' => 'application/mac-compactpro', - 'csh' => 'application/x-csh', - 'csv' => array('text/csv', 'application/vnd.ms-excel'), - 'dcr' => 'application/x-director', - 'dir' => 'application/x-director', - 'dms' => 'application/octet-stream', - 'doc' => 'application/msword', - 'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', - 'drw' => 'application/drafting', - 'dvi' => 'application/x-dvi', - 'dwg' => 'application/acad', - 'dxf' => 'application/dxf', - 'dxr' => 'application/x-director', - 'eot' => 'application/vnd.ms-fontobject', - 'eps' => 'application/postscript', - 'exe' => 'application/octet-stream', - 'ez' => 'application/andrew-inset', - 'flv' => 'video/x-flv', - 'gtar' => 'application/x-gtar', - 'gz' => 'application/x-gzip', - 'bz2' => 'application/x-bzip', - '7z' => 'application/x-7z-compressed', - 'hdf' => 'application/x-hdf', - 'hqx' => 'application/mac-binhex40', - 'ico' => 'image/x-icon', - 'ips' => 'application/x-ipscript', - 'ipx' => 'application/x-ipix', - 'js' => 'application/javascript', - 'jsonapi' => 'application/vnd.api+json', - 'latex' => 'application/x-latex', - 'lha' => 'application/octet-stream', - 'lsp' => 'application/x-lisp', - 'lzh' => 'application/octet-stream', - 'man' => 'application/x-troff-man', - 'me' => 'application/x-troff-me', - 'mif' => 'application/vnd.mif', - 'ms' => 'application/x-troff-ms', - 'nc' => 'application/x-netcdf', - 'oda' => 'application/oda', - 'otf' => 'font/otf', - 'pdf' => 'application/pdf', - 'pgn' => 'application/x-chess-pgn', - 'pot' => 'application/vnd.ms-powerpoint', - 'pps' => 'application/vnd.ms-powerpoint', - 'ppt' => 'application/vnd.ms-powerpoint', - 'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation', - 'ppz' => 'application/vnd.ms-powerpoint', - 'pre' => 'application/x-freelance', - 'prt' => 'application/pro_eng', - 'ps' => 'application/postscript', - 'roff' => 'application/x-troff', - 'scm' => 'application/x-lotusscreencam', - 'set' => 'application/set', - 'sh' => 'application/x-sh', - 'shar' => 'application/x-shar', - 'sit' => 'application/x-stuffit', - 'skd' => 'application/x-koan', - 'skm' => 'application/x-koan', - 'skp' => 'application/x-koan', - 'skt' => 'application/x-koan', - 'smi' => 'application/smil', - 'smil' => 'application/smil', - 'sol' => 'application/solids', - 'spl' => 'application/x-futuresplash', - 'src' => 'application/x-wais-source', - 'step' => 'application/STEP', - 'stl' => 'application/SLA', - 'stp' => 'application/STEP', - 'sv4cpio' => 'application/x-sv4cpio', - 'sv4crc' => 'application/x-sv4crc', - 'svg' => 'image/svg+xml', - 'svgz' => 'image/svg+xml', - 'swf' => 'application/x-shockwave-flash', - 't' => 'application/x-troff', - 'tar' => 'application/x-tar', - 'tcl' => 'application/x-tcl', - 'tex' => 'application/x-tex', - 'texi' => 'application/x-texinfo', - 'texinfo' => 'application/x-texinfo', - 'tr' => 'application/x-troff', - 'tsp' => 'application/dsptype', - 'ttc' => 'font/ttf', - 'ttf' => 'font/ttf', - 'unv' => 'application/i-deas', - 'ustar' => 'application/x-ustar', - 'vcd' => 'application/x-cdlink', - 'vda' => 'application/vda', - 'xlc' => 'application/vnd.ms-excel', - 'xll' => 'application/vnd.ms-excel', - 'xlm' => 'application/vnd.ms-excel', - 'xls' => 'application/vnd.ms-excel', - 'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', - 'xlw' => 'application/vnd.ms-excel', - 'zip' => 'application/zip', - 'aif' => 'audio/x-aiff', - 'aifc' => 'audio/x-aiff', - 'aiff' => 'audio/x-aiff', - 'au' => 'audio/basic', - 'kar' => 'audio/midi', - 'mid' => 'audio/midi', - 'midi' => 'audio/midi', - 'mp2' => 'audio/mpeg', - 'mp3' => 'audio/mpeg', - 'mpga' => 'audio/mpeg', - 'ogg' => 'audio/ogg', - 'oga' => 'audio/ogg', - 'spx' => 'audio/ogg', - 'ra' => 'audio/x-realaudio', - 'ram' => 'audio/x-pn-realaudio', - 'rm' => 'audio/x-pn-realaudio', - 'rpm' => 'audio/x-pn-realaudio-plugin', - 'snd' => 'audio/basic', - 'tsi' => 'audio/TSP-audio', - 'wav' => 'audio/x-wav', - 'aac' => 'audio/aac', - 'asc' => 'text/plain', - 'c' => 'text/plain', - 'cc' => 'text/plain', - 'css' => 'text/css', - 'etx' => 'text/x-setext', - 'f' => 'text/plain', - 'f90' => 'text/plain', - 'h' => 'text/plain', - 'hh' => 'text/plain', - 'htm' => array('text/html', '*/*'), - 'ics' => 'text/calendar', - 'm' => 'text/plain', - 'rtf' => 'text/rtf', - 'rtx' => 'text/richtext', - 'sgm' => 'text/sgml', - 'sgml' => 'text/sgml', - 'tsv' => 'text/tab-separated-values', - 'tpl' => 'text/template', - 'txt' => 'text/plain', - 'text' => 'text/plain', - 'avi' => 'video/x-msvideo', - 'fli' => 'video/x-fli', - 'mov' => 'video/quicktime', - 'movie' => 'video/x-sgi-movie', - 'mpe' => 'video/mpeg', - 'mpeg' => 'video/mpeg', - 'mpg' => 'video/mpeg', - 'qt' => 'video/quicktime', - 'viv' => 'video/vnd.vivo', - 'vivo' => 'video/vnd.vivo', - 'ogv' => 'video/ogg', - 'webm' => 'video/webm', - 'mp4' => 'video/mp4', - 'm4v' => 'video/mp4', - 'f4v' => 'video/mp4', - 'f4p' => 'video/mp4', - 'm4a' => 'audio/mp4', - 'f4a' => 'audio/mp4', - 'f4b' => 'audio/mp4', - 'gif' => 'image/gif', - 'ief' => 'image/ief', - 'jpg' => 'image/jpeg', - 'jpeg' => 'image/jpeg', - 'jpe' => 'image/jpeg', - 'pbm' => 'image/x-portable-bitmap', - 'pgm' => 'image/x-portable-graymap', - 'png' => 'image/png', - 'pnm' => 'image/x-portable-anymap', - 'ppm' => 'image/x-portable-pixmap', - 'ras' => 'image/cmu-raster', - 'rgb' => 'image/x-rgb', - 'tif' => 'image/tiff', - 'tiff' => 'image/tiff', - 'xbm' => 'image/x-xbitmap', - 'xpm' => 'image/x-xpixmap', - 'xwd' => 'image/x-xwindowdump', - 'psd' => array( - 'application/photoshop', - 'application/psd', - 'image/psd', - 'image/x-photoshop', - 'image/photoshop', - 'zz-application/zz-winassoc-psd' - ), - 'ice' => 'x-conference/x-cooltalk', - 'iges' => 'model/iges', - 'igs' => 'model/iges', - 'mesh' => 'model/mesh', - 'msh' => 'model/mesh', - 'silo' => 'model/mesh', - 'vrml' => 'model/vrml', - 'wrl' => 'model/vrml', - 'mime' => 'www/mime', - 'pdb' => 'chemical/x-pdb', - 'xyz' => 'chemical/x-pdb', - 'javascript' => 'application/javascript', - 'form' => 'application/x-www-form-urlencoded', - 'file' => 'multipart/form-data', - 'xhtml' => array('application/xhtml+xml', 'application/xhtml', 'text/xhtml'), - 'xhtml-mobile' => 'application/vnd.wap.xhtml+xml', - 'atom' => 'application/atom+xml', - 'amf' => 'application/x-amf', - 'wap' => array('text/vnd.wap.wml', 'text/vnd.wap.wmlscript', 'image/vnd.wap.wbmp'), - 'wml' => 'text/vnd.wap.wml', - 'wmlscript' => 'text/vnd.wap.wmlscript', - 'wbmp' => 'image/vnd.wap.wbmp', - 'woff' => 'application/x-font-woff', - 'webp' => 'image/webp', - 'appcache' => 'text/cache-manifest', - 'manifest' => 'text/cache-manifest', - 'htc' => 'text/x-component', - 'rdf' => 'application/xml', - 'crx' => 'application/x-chrome-extension', - 'oex' => 'application/x-opera-extension', - 'xpi' => 'application/x-xpinstall', - 'safariextz' => 'application/octet-stream', - 'webapp' => 'application/x-web-app-manifest+json', - 'vcf' => 'text/x-vcard', - 'vtt' => 'text/vtt', - 'mkv' => 'video/x-matroska', - 'pkpass' => 'application/vnd.apple.pkpass', - 'ajax' => 'text/html' - ); - -/** - * Protocol header to send to the client - * - * @var string - */ - protected $_protocol = 'HTTP/1.1'; - -/** - * Status code to send to the client - * - * @var int - */ - protected $_status = 200; - -/** - * Content type to send. This can be an 'extension' that will be transformed using the $_mimetypes array - * or a complete mime-type - * - * @var string - */ - protected $_contentType = 'text/html'; - -/** - * Buffer list of headers - * - * @var array - */ - protected $_headers = array(); - -/** - * Buffer string for response message - * - * @var string - */ - protected $_body = null; - -/** - * File object for file to be read out as response - * - * @var File - */ - protected $_file = null; - -/** - * File range. Used for requesting ranges of files. - * - * @var array - */ - protected $_fileRange = null; - -/** - * The charset the response body is encoded with - * - * @var string - */ - protected $_charset = 'UTF-8'; - -/** - * Holds all the cache directives that will be converted - * into headers when sending the request - * - * @var array - */ - protected $_cacheDirectives = array(); - -/** - * Holds cookies to be sent to the client - * - * @var array - */ - protected $_cookies = array(); - -/** - * Constructor - * - * @param array $options list of parameters to setup the response. Possible values are: - * - body: the response text that should be sent to the client - * - statusCodes: additional allowable response codes - * - status: the HTTP status code to respond with - * - type: a complete mime-type string or an extension mapped in this class - * - charset: the charset for the response body - */ - public function __construct(array $options = array()) { - if (isset($options['body'])) { - $this->body($options['body']); - } - if (isset($options['statusCodes'])) { - $this->httpCodes($options['statusCodes']); - } - if (isset($options['status'])) { - $this->statusCode($options['status']); - } - if (isset($options['type'])) { - $this->type($options['type']); - } - if (!isset($options['charset'])) { - $options['charset'] = Configure::read('App.encoding'); - } - $this->charset($options['charset']); - } - -/** - * Sends the complete response to the client including headers and message body. - * Will echo out the content in the response body. - * - * @return void - */ - public function send() { - if (isset($this->_headers['Location']) && $this->_status === 200) { - $this->statusCode(302); - } - - $codeMessage = $this->_statusCodes[$this->_status]; - $this->_setCookies(); - $this->_sendHeader("{$this->_protocol} {$this->_status} {$codeMessage}"); - $this->_setContent(); - $this->_setContentLength(); - $this->_setContentType(); - foreach ($this->_headers as $header => $values) { - foreach ((array)$values as $value) { - $this->_sendHeader($header, $value); - } - } - if ($this->_file) { - $this->_sendFile($this->_file, $this->_fileRange); - $this->_file = $this->_fileRange = null; - } else { - $this->_sendContent($this->_body); - } - } - -/** - * Sets the cookies that have been added via CakeResponse::cookie() before any - * other output is sent to the client. Will set the cookies in the order they - * have been set. - * - * @return void - */ - protected function _setCookies() { - foreach ($this->_cookies as $name => $c) { - setcookie( - $name, $c['value'], $c['expire'], $c['path'], - $c['domain'], $c['secure'], $c['httpOnly'] - ); - } - } - -/** - * Formats the Content-Type header based on the configured contentType and charset - * the charset will only be set in the header if the response is of type text - * - * @return void - */ - protected function _setContentType() { - if (in_array($this->_status, array(304, 204))) { - return; - } - $whitelist = array( - 'application/javascript', 'application/json', 'application/xml', 'application/rss+xml' - ); - - $charset = false; - if ($this->_charset && - (strpos($this->_contentType, 'text/') === 0 || in_array($this->_contentType, $whitelist)) - ) { - $charset = true; - } - - if ($charset) { - $this->header('Content-Type', "{$this->_contentType}; charset={$this->_charset}"); - } else { - $this->header('Content-Type', "{$this->_contentType}"); - } - } - -/** - * Sets the response body to an empty text if the status code is 204 or 304 - * - * @return void - */ - protected function _setContent() { - if (in_array($this->_status, array(304, 204))) { - $this->body(''); - } - } - -/** - * Calculates the correct Content-Length and sets it as a header in the response - * Will not set the value if already set or if the output is compressed. - * - * @return void - */ - protected function _setContentLength() { - $shouldSetLength = !isset($this->_headers['Content-Length']) && !in_array($this->_status, range(301, 307)); - if (isset($this->_headers['Content-Length']) && $this->_headers['Content-Length'] === false) { - unset($this->_headers['Content-Length']); - return; - } - if ($shouldSetLength && !$this->outputCompressed()) { - $offset = ob_get_level() ? ob_get_length() : 0; - if (ini_get('mbstring.func_overload') & 2 && function_exists('mb_strlen')) { - $this->length($offset + mb_strlen($this->_body, '8bit')); - } else { - $this->length($this->_headers['Content-Length'] = $offset + strlen($this->_body)); - } - } - } - -/** - * Sends a header to the client. - * - * Will skip sending headers if headers have already been sent. - * - * @param string $name the header name - * @param string $value the header value - * @return void - */ - protected function _sendHeader($name, $value = null) { - if (headers_sent($filename, $linenum)) { - return; - } - if ($value === null) { - header($name); - } else { - header("{$name}: {$value}"); - } - } - -/** - * Sends a content string to the client. - * - * @param string $content string to send as response body - * @return void - */ - protected function _sendContent($content) { - echo $content; - } - -/** - * Buffers a header string to be sent - * Returns the complete list of buffered headers - * - * ### Single header - * e.g `header('Location', 'http://example.com');` - * - * ### Multiple headers - * e.g `header(array('Location' => 'http://example.com', 'X-Extra' => 'My header'));` - * - * ### String header - * e.g `header('WWW-Authenticate: Negotiate');` - * - * ### Array of string headers - * e.g `header(array('WWW-Authenticate: Negotiate', 'Content-type: application/pdf'));` - * - * Multiple calls for setting the same header name will have the same effect as setting the header once - * with the last value sent for it - * e.g `header('WWW-Authenticate: Negotiate'); header('WWW-Authenticate: Not-Negotiate');` - * will have the same effect as only doing `header('WWW-Authenticate: Not-Negotiate');` - * - * @param string|array $header An array of header strings or a single header string - * - an associative array of "header name" => "header value" is also accepted - * - an array of string headers is also accepted - * @param string|array $value The header value(s) - * @return array list of headers to be sent - */ - public function header($header = null, $value = null) { - if ($header === null) { - return $this->_headers; - } - $headers = is_array($header) ? $header : array($header => $value); - foreach ($headers as $header => $value) { - if (is_numeric($header)) { - list($header, $value) = array($value, null); - } - if ($value === null && strpos($header, ':') !== false) { - list($header, $value) = explode(':', $header, 2); - } - $this->_headers[$header] = is_array($value) ? array_map('trim', $value) : trim($value); - } - return $this->_headers; - } - -/** - * Accessor for the location header. - * - * Get/Set the Location header value. - * - * @param null|string $url Either null to get the current location, or a string to set one. - * @return string|null When setting the location null will be returned. When reading the location - * a string of the current location header value (if any) will be returned. - */ - public function location($url = null) { - if ($url === null) { - $headers = $this->header(); - return isset($headers['Location']) ? $headers['Location'] : null; - } - $this->header('Location', $url); - return null; - } - -/** - * Buffers the response message to be sent - * if $content is null the current buffer is returned - * - * @param string $content the string message to be sent - * @return string current message buffer if $content param is passed as null - */ - public function body($content = null) { - if ($content === null) { - return $this->_body; - } - return $this->_body = $content; - } - -/** - * Sets the HTTP status code to be sent - * if $code is null the current code is returned - * - * @param int $code the HTTP status code - * @return int current status code - * @throws CakeException When an unknown status code is reached. - */ - public function statusCode($code = null) { - if ($code === null) { - return $this->_status; - } - if (!isset($this->_statusCodes[$code])) { - throw new CakeException(__d('cake_dev', 'Unknown status code')); - } - return $this->_status = $code; - } - -/** - * Queries & sets valid HTTP response codes & messages. - * - * @param int|array $code If $code is an integer, then the corresponding code/message is - * returned if it exists, null if it does not exist. If $code is an array, then the - * keys are used as codes and the values as messages to add to the default HTTP - * codes. The codes must be integers greater than 99 and less than 1000. Keep in - * mind that the HTTP specification outlines that status codes begin with a digit - * between 1 and 5, which defines the class of response the client is to expect. - * Example: - * - * httpCodes(404); // returns array(404 => 'Not Found') - * - * httpCodes(array( - * 381 => 'Unicorn Moved', - * 555 => 'Unexpected Minotaur' - * )); // sets these new values, and returns true - * - * httpCodes(array( - * 0 => 'Nothing Here', - * -1 => 'Reverse Infinity', - * 12345 => 'Universal Password', - * 'Hello' => 'World' - * )); // throws an exception due to invalid codes - * - * For more on HTTP status codes see: http://www.w3.org/Protocols/rfc2616/rfc2616-sec6.html#sec6.1 - * - * @return array|null|true associative array of the HTTP codes as keys, and the message - * strings as values, or null of the given $code does not exist. `true` if `$code` is - * an array of valid codes. - * @throws CakeException If an attempt is made to add an invalid status code - */ - public function httpCodes($code = null) { - if (empty($code)) { - return $this->_statusCodes; - } - if (is_array($code)) { - $codes = array_keys($code); - $min = min($codes); - if (!is_int($min) || $min < 100 || max($codes) > 999) { - throw new CakeException(__d('cake_dev', 'Invalid status code')); - } - $this->_statusCodes = $code + $this->_statusCodes; - return true; - } - if (!isset($this->_statusCodes[$code])) { - return null; - } - return array($code => $this->_statusCodes[$code]); - } - -/** - * Sets the response content type. It can be either a file extension - * which will be mapped internally to a mime-type or a string representing a mime-type - * if $contentType is null the current content type is returned - * if $contentType is an associative array, content type definitions will be stored/replaced - * - * ### Setting the content type - * - * e.g `type('jpg');` - * - * ### Returning the current content type - * - * e.g `type();` - * - * ### Storing content type definitions - * - * e.g `type(array('keynote' => 'application/keynote', 'bat' => 'application/bat'));` - * - * ### Replacing a content type definition - * - * e.g `type(array('jpg' => 'text/plain'));` - * - * @param array|string|null $contentType Content type key. - * @return string|false current content type or false if supplied an invalid content type - */ - public function type($contentType = null) { - if ($contentType === null) { - return $this->_contentType; - } - if (is_array($contentType)) { - foreach ($contentType as $type => $definition) { - $this->_mimeTypes[$type] = $definition; - } - return $this->_contentType; - } - if (isset($this->_mimeTypes[$contentType])) { - $contentType = $this->_mimeTypes[$contentType]; - $contentType = is_array($contentType) ? current($contentType) : $contentType; - } - if (strpos($contentType, '/') === false) { - return false; - } - return $this->_contentType = $contentType; - } - -/** - * Returns the mime type definition for an alias - * - * e.g `getMimeType('pdf'); // returns 'application/pdf'` - * - * @param string $alias the content type alias to map - * @return mixed string mapped mime type or false if $alias is not mapped - */ - public function getMimeType($alias) { - if (isset($this->_mimeTypes[$alias])) { - return $this->_mimeTypes[$alias]; - } - return false; - } - -/** - * Maps a content-type back to an alias - * - * e.g `mapType('application/pdf'); // returns 'pdf'` - * - * @param string|array $ctype Either a string content type to map, or an array of types. - * @return mixed Aliases for the types provided. - */ - public function mapType($ctype) { - if (is_array($ctype)) { - return array_map(array($this, 'mapType'), $ctype); - } - - foreach ($this->_mimeTypes as $alias => $types) { - if (in_array($ctype, (array)$types)) { - return $alias; - } - } - return null; - } - -/** - * Sets the response charset - * if $charset is null the current charset is returned - * - * @param string $charset Character set string. - * @return string current charset - */ - public function charset($charset = null) { - if ($charset === null) { - return $this->_charset; - } - return $this->_charset = $charset; - } - -/** - * Sets the correct headers to instruct the client to not cache the response - * - * @return void - */ - public function disableCache() { - $this->header(array( - 'Expires' => 'Mon, 26 Jul 1997 05:00:00 GMT', - 'Last-Modified' => gmdate("D, d M Y H:i:s") . " GMT", - 'Cache-Control' => 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0' - )); - } - -/** - * Sets the correct headers to instruct the client to cache the response. - * - * @param string|int $since a valid time since the response text has not been modified - * @param string|int $time a valid time for cache expiry - * @return void - */ - public function cache($since, $time = '+1 day') { - if (!is_int($time)) { - $time = strtotime($time); - } - $this->header(array( - 'Date' => gmdate("D, j M Y G:i:s ", time()) . 'GMT' - )); - $this->modified($since); - $this->expires($time); - $this->sharable(true); - $this->maxAge($time - time()); - } - -/** - * Sets whether a response is eligible to be cached by intermediate proxies - * This method controls the `public` or `private` directive in the Cache-Control - * header - * - * @param bool $public If set to true, the Cache-Control header will be set as public - * if set to false, the response will be set to private - * if no value is provided, it will return whether the response is sharable or not - * @param int $time time in seconds after which the response should no longer be considered fresh - * @return bool - */ - public function sharable($public = null, $time = null) { - if ($public === null) { - $public = array_key_exists('public', $this->_cacheDirectives); - $private = array_key_exists('private', $this->_cacheDirectives); - $noCache = array_key_exists('no-cache', $this->_cacheDirectives); - if (!$public && !$private && !$noCache) { - return null; - } - $sharable = $public || !($private || $noCache); - return $sharable; - } - if ($public) { - $this->_cacheDirectives['public'] = true; - unset($this->_cacheDirectives['private']); - } else { - $this->_cacheDirectives['private'] = true; - unset($this->_cacheDirectives['public']); - } - - $this->maxAge($time); - if ((int)$time === 0) { - $this->_setCacheControl(); - } - return (bool)$public; - } - -/** - * Sets the Cache-Control s-maxage directive. - * The max-age is the number of seconds after which the response should no longer be considered - * a good candidate to be fetched from a shared cache (like in a proxy server). - * If called with no parameters, this function will return the current max-age value if any - * - * @param int $seconds if null, the method will return the current s-maxage value - * @return int - */ - public function sharedMaxAge($seconds = null) { - if ($seconds !== null) { - $this->_cacheDirectives['s-maxage'] = $seconds; - $this->_setCacheControl(); - } - if (isset($this->_cacheDirectives['s-maxage'])) { - return $this->_cacheDirectives['s-maxage']; - } - return null; - } - -/** - * Sets the Cache-Control max-age directive. - * The max-age is the number of seconds after which the response should no longer be considered - * a good candidate to be fetched from the local (client) cache. - * If called with no parameters, this function will return the current max-age value if any - * - * @param int $seconds if null, the method will return the current max-age value - * @return int - */ - public function maxAge($seconds = null) { - if ($seconds !== null) { - $this->_cacheDirectives['max-age'] = $seconds; - $this->_setCacheControl(); - } - if (isset($this->_cacheDirectives['max-age'])) { - return $this->_cacheDirectives['max-age']; - } - return null; - } - -/** - * Sets the Cache-Control must-revalidate directive. - * must-revalidate indicates that the response should not be served - * stale by a cache under any circumstance without first revalidating - * with the origin. - * If called with no parameters, this function will return whether must-revalidate is present. - * - * @param bool $enable If null returns whether directive is set, if boolean - * sets or unsets directive. - * @return bool - */ - public function mustRevalidate($enable = null) { - if ($enable !== null) { - if ($enable) { - $this->_cacheDirectives['must-revalidate'] = true; - } else { - unset($this->_cacheDirectives['must-revalidate']); - } - $this->_setCacheControl(); - } - return array_key_exists('must-revalidate', $this->_cacheDirectives); - } - -/** - * Helper method to generate a valid Cache-Control header from the options set - * in other methods - * - * @return void - */ - protected function _setCacheControl() { - $control = ''; - foreach ($this->_cacheDirectives as $key => $val) { - $control .= $val === true ? $key : sprintf('%s=%s', $key, $val); - $control .= ', '; - } - $control = rtrim($control, ', '); - $this->header('Cache-Control', $control); - } - -/** - * Sets the Expires header for the response by taking an expiration time - * If called with no parameters it will return the current Expires value - * - * ## Examples: - * - * `$response->expires('now')` Will Expire the response cache now - * `$response->expires(new DateTime('+1 day'))` Will set the expiration in next 24 hours - * `$response->expires()` Will return the current expiration header value - * - * @param string|DateTime $time Valid time string or DateTime object. - * @return string - */ - public function expires($time = null) { - if ($time !== null) { - $date = $this->_getUTCDate($time); - $this->_headers['Expires'] = $date->format('D, j M Y H:i:s') . ' GMT'; - } - if (isset($this->_headers['Expires'])) { - return $this->_headers['Expires']; - } - return null; - } - -/** - * Sets the Last-Modified header for the response by taking a modification time - * If called with no parameters it will return the current Last-Modified value - * - * ## Examples: - * - * `$response->modified('now')` Will set the Last-Modified to the current time - * `$response->modified(new DateTime('+1 day'))` Will set the modification date in the past 24 hours - * `$response->modified()` Will return the current Last-Modified header value - * - * @param string|DateTime $time Valid time string or DateTime object. - * @return string - */ - public function modified($time = null) { - if ($time !== null) { - $date = $this->_getUTCDate($time); - $this->_headers['Last-Modified'] = $date->format('D, j M Y H:i:s') . ' GMT'; - } - if (isset($this->_headers['Last-Modified'])) { - return $this->_headers['Last-Modified']; - } - return null; - } - -/** - * Sets the response as Not Modified by removing any body contents - * setting the status code to "304 Not Modified" and removing all - * conflicting headers - * - * @return void - */ - public function notModified() { - $this->statusCode(304); - $this->body(''); - $remove = array( - 'Allow', - 'Content-Encoding', - 'Content-Language', - 'Content-Length', - 'Content-MD5', - 'Content-Type', - 'Last-Modified' - ); - foreach ($remove as $header) { - unset($this->_headers[$header]); - } - } - -/** - * Sets the Vary header for the response, if an array is passed, - * values will be imploded into a comma separated string. If no - * parameters are passed, then an array with the current Vary header - * value is returned - * - * @param string|array $cacheVariances a single Vary string or an array - * containing the list for variances. - * @return array - */ - public function vary($cacheVariances = null) { - if ($cacheVariances !== null) { - $cacheVariances = (array)$cacheVariances; - $this->_headers['Vary'] = implode(', ', $cacheVariances); - } - if (isset($this->_headers['Vary'])) { - return explode(', ', $this->_headers['Vary']); - } - return null; - } - -/** - * Sets the response Etag, Etags are a strong indicative that a response - * can be cached by a HTTP client. A bad way of generating Etags is - * creating a hash of the response output, instead generate a unique - * hash of the unique components that identifies a request, such as a - * modification time, a resource Id, and anything else you consider it - * makes it unique. - * - * Second parameter is used to instruct clients that the content has - * changed, but sematicallly, it can be used as the same thing. Think - * for instance of a page with a hit counter, two different page views - * are equivalent, but they differ by a few bytes. This leaves off to - * the Client the decision of using or not the cached page. - * - * If no parameters are passed, current Etag header is returned. - * - * @param string $tag Tag to set. - * @param bool $weak whether the response is semantically the same as - * other with the same hash or not - * @return string - */ - public function etag($tag = null, $weak = false) { - if ($tag !== null) { - $this->_headers['Etag'] = sprintf('%s"%s"', ($weak) ? 'W/' : null, $tag); - } - if (isset($this->_headers['Etag'])) { - return $this->_headers['Etag']; - } - return null; - } - -/** - * Returns a DateTime object initialized at the $time param and using UTC - * as timezone - * - * @param DateTime|int|string $time Valid time string or unix timestamp or DateTime object. - * @return DateTime - */ - protected function _getUTCDate($time = null) { - if ($time instanceof DateTime) { - $result = clone $time; - } elseif (is_int($time)) { - $result = new DateTime(date('Y-m-d H:i:s', $time)); - } else { - $result = new DateTime($time); - } - $result->setTimeZone(new DateTimeZone('UTC')); - return $result; - } - -/** - * Sets the correct output buffering handler to send a compressed response. Responses will - * be compressed with zlib, if the extension is available. - * - * @return bool false if client does not accept compressed responses or no handler is available, true otherwise - */ - public function compress() { - $compressionEnabled = ini_get("zlib.output_compression") !== '1' && - extension_loaded("zlib") && - (strpos(env('HTTP_ACCEPT_ENCODING'), 'gzip') !== false); - return $compressionEnabled && ob_start('ob_gzhandler'); - } - -/** - * Returns whether the resulting output will be compressed by PHP - * - * @return bool - */ - public function outputCompressed() { - return strpos(env('HTTP_ACCEPT_ENCODING'), 'gzip') !== false - && (ini_get("zlib.output_compression") === '1' || in_array('ob_gzhandler', ob_list_handlers())); - } - -/** - * Sets the correct headers to instruct the browser to download the response as a file. - * - * @param string $filename the name of the file as the browser will download the response - * @return void - */ - public function download($filename) { - $this->header('Content-Disposition', 'attachment; filename="' . $filename . '"'); - } - -/** - * Sets the protocol to be used when sending the response. Defaults to HTTP/1.1 - * If called with no arguments, it will return the current configured protocol - * - * @param string $protocol Protocol to be used for sending response. - * @return string protocol currently set - */ - public function protocol($protocol = null) { - if ($protocol !== null) { - $this->_protocol = $protocol; - } - return $this->_protocol; - } - -/** - * Sets the Content-Length header for the response - * If called with no arguments returns the last Content-Length set - * - * @param int $bytes Number of bytes - * @return int|null - */ - public function length($bytes = null) { - if ($bytes !== null) { - $this->_headers['Content-Length'] = $bytes; - } - if (isset($this->_headers['Content-Length'])) { - return $this->_headers['Content-Length']; - } - return null; - } - -/** - * Checks whether a response has not been modified according to the 'If-None-Match' - * (Etags) and 'If-Modified-Since' (last modification date) request - * headers. If the response is detected to be not modified, it - * is marked as so accordingly so the client can be informed of that. - * - * In order to mark a response as not modified, you need to set at least - * the Last-Modified etag response header before calling this method. Otherwise - * a comparison will not be possible. - * - * @param CakeRequest $request Request object - * @return bool whether the response was marked as not modified or not. - */ - public function checkNotModified(CakeRequest $request) { - $ifNoneMatchHeader = $request->header('If-None-Match'); - $etags = array(); - if (is_string($ifNoneMatchHeader)) { - $etags = preg_split('/\s*,\s*/', $ifNoneMatchHeader, null, PREG_SPLIT_NO_EMPTY); - } - $modifiedSince = $request->header('If-Modified-Since'); - $checks = array(); - if ($responseTag = $this->etag()) { - $checks[] = in_array('*', $etags) || in_array($responseTag, $etags); - } - if ($modifiedSince) { - $checks[] = strtotime($this->modified()) === strtotime($modifiedSince); - } - if (empty($checks)) { - return false; - } - $notModified = !in_array(false, $checks, true); - if ($notModified) { - $this->notModified(); - } - return $notModified; - } - -/** - * String conversion. Fetches the response body as a string. - * Does *not* send headers. - * - * @return string - */ - public function __toString() { - return (string)$this->_body; - } - -/** - * Getter/Setter for cookie configs - * - * This method acts as a setter/getter depending on the type of the argument. - * If the method is called with no arguments, it returns all configurations. - * - * If the method is called with a string as argument, it returns either the - * given configuration if it is set, or null, if it's not set. - * - * If the method is called with an array as argument, it will set the cookie - * configuration to the cookie container. - * - * ### Options (when setting a configuration) - * - name: The Cookie name - * - value: Value of the cookie - * - expire: Time the cookie expires in - * - path: Path the cookie applies to - * - domain: Domain the cookie is for. - * - secure: Is the cookie https? - * - httpOnly: Is the cookie available in the client? - * - * ## Examples - * - * ### Getting all cookies - * - * `$this->cookie()` - * - * ### Getting a certain cookie configuration - * - * `$this->cookie('MyCookie')` - * - * ### Setting a cookie configuration - * - * `$this->cookie((array) $options)` - * - * @param array|string $options Either null to get all cookies, string for a specific cookie - * or array to set cookie. - * @return mixed - */ - public function cookie($options = null) { - if ($options === null) { - return $this->_cookies; - } - - if (is_string($options)) { - if (!isset($this->_cookies[$options])) { - return null; - } - return $this->_cookies[$options]; - } - - $defaults = array( - 'name' => 'CakeCookie[default]', - 'value' => '', - 'expire' => 0, - 'path' => '/', - 'domain' => '', - 'secure' => false, - 'httpOnly' => false - ); - $options += $defaults; - - $this->_cookies[$options['name']] = $options; - } - -/** - * Setup access for origin and methods on cross origin requests - * - * This method allow multiple ways to setup the domains, see the examples - * - * ### Full URI - * e.g `cors($request, 'https://www.cakephp.org');` - * - * ### URI with wildcard - * e.g `cors($request, 'http://*.cakephp.org');` - * - * ### Ignoring the requested protocol - * e.g `cors($request, 'www.cakephp.org');` - * - * ### Any URI - * e.g `cors($request, '*');` - * - * ### Whitelist of URIs - * e.g `cors($request, array('https://www.cakephp.org', '*.google.com', 'https://myproject.github.io'));` - * - * @param CakeRequest $request Request object - * @param string|array $allowedDomains List of allowed domains, see method description for more details - * @param string|array $allowedMethods List of HTTP verbs allowed - * @param string|array $allowedHeaders List of HTTP headers allowed - * @return void - */ - public function cors(CakeRequest $request, $allowedDomains, $allowedMethods = array(), $allowedHeaders = array()) { - $origin = $request->header('Origin'); - if (!$origin) { - return; - } - - $allowedDomains = $this->_normalizeCorsDomains((array)$allowedDomains, $request->is('ssl')); - foreach ($allowedDomains as $domain) { - if (!preg_match($domain['preg'], $origin)) { - continue; - } - $this->header('Access-Control-Allow-Origin', $domain['original'] === '*' ? '*' : $origin); - $allowedMethods && $this->header('Access-Control-Allow-Methods', implode(', ', (array)$allowedMethods)); - $allowedHeaders && $this->header('Access-Control-Allow-Headers', implode(', ', (array)$allowedHeaders)); - break; - } - } - -/** - * Normalize the origin to regular expressions and put in an array format - * - * @param array $domains Domains to normalize - * @param bool $requestIsSSL Whether it's a SSL request. - * @return array - */ - protected function _normalizeCorsDomains($domains, $requestIsSSL = false) { - $result = array(); - foreach ($domains as $domain) { - if ($domain === '*') { - $result[] = array('preg' => '@.@', 'original' => '*'); - continue; - } - $original = $domain; - $preg = '@' . str_replace('*', '.*', $domain) . '@'; - $result[] = compact('original', 'preg'); - } - return $result; - } - -/** - * Setup for display or download the given file. - * - * If $_SERVER['HTTP_RANGE'] is set a slice of the file will be - * returned instead of the entire file. - * - * ### Options keys - * - * - name: Alternate download name - * - download: If `true` sets download header and forces file to be downloaded rather than displayed in browser - * - * @param string $path Path to file. If the path is not an absolute path that resolves - * to a file, `APP` will be prepended to the path. - * @param array $options Options See above. - * @return void - * @throws NotFoundException - */ - public function file($path, $options = array()) { - $options += array( - 'name' => null, - 'download' => null - ); - - if (strpos($path, '../') !== false || strpos($path, '..\\') !== false) { - throw new NotFoundException(__d( - 'cake_dev', - 'The requested file contains `..` and will not be read.' - )); - } - - if (!is_file($path)) { - $path = APP . $path; - } - - $file = new File($path); - if (!$file->exists() || !$file->readable()) { - if (Configure::read('debug')) { - throw new NotFoundException(__d('cake_dev', 'The requested file %s was not found or not readable', $path)); - } - throw new NotFoundException(__d('cake', 'The requested file was not found')); - } - - $extension = strtolower($file->ext()); - $download = $options['download']; - if ((!$extension || $this->type($extension) === false) && $download === null) { - $download = true; - } - - $fileSize = $file->size(); - if ($download) { - $agent = env('HTTP_USER_AGENT'); - - if (preg_match('%Opera(/| )([0-9].[0-9]{1,2})%', $agent)) { - $contentType = 'application/octet-stream'; - } elseif (preg_match('/MSIE ([0-9].[0-9]{1,2})/', $agent)) { - $contentType = 'application/force-download'; - } - - if (!empty($contentType)) { - $this->type($contentType); - } - if ($options['name'] === null) { - $name = $file->name; - } else { - $name = $options['name']; - } - $this->download($name); - $this->header('Content-Transfer-Encoding', 'binary'); - } - - $this->header('Accept-Ranges', 'bytes'); - $httpRange = env('HTTP_RANGE'); - if (isset($httpRange)) { - $this->_fileRange($file, $httpRange); - } else { - $this->header('Content-Length', $fileSize); - } - - $this->_clearBuffer(); - $this->_file = $file; - } - -/** - * Apply a file range to a file and set the end offset. - * - * If an invalid range is requested a 416 Status code will be used - * in the response. - * - * @param File $file The file to set a range on. - * @param string $httpRange The range to use. - * @return void - */ - protected function _fileRange($file, $httpRange) { - $fileSize = $file->size(); - $lastByte = $fileSize - 1; - $start = 0; - $end = $lastByte; - - preg_match('/^bytes\s*=\s*(\d+)?\s*-\s*(\d+)?$/', $httpRange, $matches); - if ($matches) { - $start = $matches[1]; - $end = isset($matches[2]) ? $matches[2] : ''; - } - - if ($start === '') { - $start = $fileSize - $end; - $end = $lastByte; - } - if ($end === '') { - $end = $lastByte; - } - - if ($start > $end || $end > $lastByte || $start > $lastByte) { - $this->statusCode(416); - $this->header(array( - 'Content-Range' => 'bytes 0-' . $lastByte . '/' . $fileSize - )); - return; - } - - $this->header(array( - 'Content-Length' => $end - $start + 1, - 'Content-Range' => 'bytes ' . $start . '-' . $end . '/' . $fileSize - )); - - $this->statusCode(206); - $this->_fileRange = array($start, $end); - } - -/** - * Reads out a file, and echos the content to the client. - * - * @param File $file File object - * @param array $range The range to read out of the file. - * @return bool True is whole file is echoed successfully or false if client connection is lost in between - */ - protected function _sendFile($file, $range) { - $compress = $this->outputCompressed(); - $file->open('rb'); - - $end = $start = false; - if ($range && is_array($range)) { - list($start, $end) = $range; - } - if ($start !== false) { - $file->offset($start); - } - - $bufferSize = 8192; - set_time_limit(0); - session_write_close(); - while (!feof($file->handle)) { - if (!$this->_isActive()) { - $file->close(); - return false; - } - $offset = $file->offset(); - if ($end && $offset >= $end) { - break; - } - if ($end && $offset + $bufferSize >= $end) { - $bufferSize = $end - $offset + 1; - } - echo fread($file->handle, $bufferSize); - if (!$compress) { - $this->_flushBuffer(); - } - } - $file->close(); - return true; - } - -/** - * Returns true if connection is still active - * - * @return bool - */ - protected function _isActive() { - return connection_status() === CONNECTION_NORMAL && !connection_aborted(); - } - -/** - * Clears the contents of the topmost output buffer and discards them - * - * @return bool - */ - protected function _clearBuffer() { - if (ob_get_length()) { - return ob_end_clean(); - } - return true; - } - -/** - * Flushes the contents of the output buffer - * - * @return void - */ - protected function _flushBuffer() { - //@codingStandardsIgnoreStart - @flush(); - if (ob_get_level()) { - @ob_flush(); - } - //@codingStandardsIgnoreEnd - } +class CakeResponse +{ + + /** + * Holds HTTP response statuses + * + * @var array + */ + protected $_statusCodes = [ + 100 => 'Continue', + 101 => 'Switching Protocols', + 200 => 'OK', + 201 => 'Created', + 202 => 'Accepted', + 203 => 'Non-Authoritative Information', + 204 => 'No Content', + 205 => 'Reset Content', + 206 => 'Partial Content', + 300 => 'Multiple Choices', + 301 => 'Moved Permanently', + 302 => 'Found', + 303 => 'See Other', + 304 => 'Not Modified', + 305 => 'Use Proxy', + 307 => 'Temporary Redirect', + 400 => 'Bad Request', + 401 => 'Unauthorized', + 402 => 'Payment Required', + 403 => 'Forbidden', + 404 => 'Not Found', + 405 => 'Method Not Allowed', + 406 => 'Not Acceptable', + 407 => 'Proxy Authentication Required', + 408 => 'Request Time-out', + 409 => 'Conflict', + 410 => 'Gone', + 411 => 'Length Required', + 412 => 'Precondition Failed', + 413 => 'Request Entity Too Large', + 414 => 'Request-URI Too Large', + 415 => 'Unsupported Media Type', + 416 => 'Requested range not satisfiable', + 417 => 'Expectation Failed', + 429 => 'Too Many Requests', + 500 => 'Internal Server Error', + 501 => 'Not Implemented', + 502 => 'Bad Gateway', + 503 => 'Service Unavailable', + 504 => 'Gateway Time-out', + 505 => 'Unsupported Version' + ]; + + /** + * Holds known mime type mappings + * + * @var array + */ + protected $_mimeTypes = [ + 'html' => ['text/html', '*/*'], + 'json' => 'application/json', + 'xml' => ['application/xml', 'text/xml'], + 'rss' => 'application/rss+xml', + 'ai' => 'application/postscript', + 'bcpio' => 'application/x-bcpio', + 'bin' => 'application/octet-stream', + 'ccad' => 'application/clariscad', + 'cdf' => 'application/x-netcdf', + 'class' => 'application/octet-stream', + 'cpio' => 'application/x-cpio', + 'cpt' => 'application/mac-compactpro', + 'csh' => 'application/x-csh', + 'csv' => ['text/csv', 'application/vnd.ms-excel'], + 'dcr' => 'application/x-director', + 'dir' => 'application/x-director', + 'dms' => 'application/octet-stream', + 'doc' => 'application/msword', + 'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'drw' => 'application/drafting', + 'dvi' => 'application/x-dvi', + 'dwg' => 'application/acad', + 'dxf' => 'application/dxf', + 'dxr' => 'application/x-director', + 'eot' => 'application/vnd.ms-fontobject', + 'eps' => 'application/postscript', + 'exe' => 'application/octet-stream', + 'ez' => 'application/andrew-inset', + 'flv' => 'video/x-flv', + 'gtar' => 'application/x-gtar', + 'gz' => 'application/x-gzip', + 'bz2' => 'application/x-bzip', + '7z' => 'application/x-7z-compressed', + 'hdf' => 'application/x-hdf', + 'hqx' => 'application/mac-binhex40', + 'ico' => 'image/x-icon', + 'ips' => 'application/x-ipscript', + 'ipx' => 'application/x-ipix', + 'js' => 'application/javascript', + 'jsonapi' => 'application/vnd.api+json', + 'latex' => 'application/x-latex', + 'lha' => 'application/octet-stream', + 'lsp' => 'application/x-lisp', + 'lzh' => 'application/octet-stream', + 'man' => 'application/x-troff-man', + 'me' => 'application/x-troff-me', + 'mif' => 'application/vnd.mif', + 'ms' => 'application/x-troff-ms', + 'nc' => 'application/x-netcdf', + 'oda' => 'application/oda', + 'otf' => 'font/otf', + 'pdf' => 'application/pdf', + 'pgn' => 'application/x-chess-pgn', + 'pot' => 'application/vnd.ms-powerpoint', + 'pps' => 'application/vnd.ms-powerpoint', + 'ppt' => 'application/vnd.ms-powerpoint', + 'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + 'ppz' => 'application/vnd.ms-powerpoint', + 'pre' => 'application/x-freelance', + 'prt' => 'application/pro_eng', + 'ps' => 'application/postscript', + 'roff' => 'application/x-troff', + 'scm' => 'application/x-lotusscreencam', + 'set' => 'application/set', + 'sh' => 'application/x-sh', + 'shar' => 'application/x-shar', + 'sit' => 'application/x-stuffit', + 'skd' => 'application/x-koan', + 'skm' => 'application/x-koan', + 'skp' => 'application/x-koan', + 'skt' => 'application/x-koan', + 'smi' => 'application/smil', + 'smil' => 'application/smil', + 'sol' => 'application/solids', + 'spl' => 'application/x-futuresplash', + 'src' => 'application/x-wais-source', + 'step' => 'application/STEP', + 'stl' => 'application/SLA', + 'stp' => 'application/STEP', + 'sv4cpio' => 'application/x-sv4cpio', + 'sv4crc' => 'application/x-sv4crc', + 'svg' => 'image/svg+xml', + 'svgz' => 'image/svg+xml', + 'swf' => 'application/x-shockwave-flash', + 't' => 'application/x-troff', + 'tar' => 'application/x-tar', + 'tcl' => 'application/x-tcl', + 'tex' => 'application/x-tex', + 'texi' => 'application/x-texinfo', + 'texinfo' => 'application/x-texinfo', + 'tr' => 'application/x-troff', + 'tsp' => 'application/dsptype', + 'ttc' => 'font/ttf', + 'ttf' => 'font/ttf', + 'unv' => 'application/i-deas', + 'ustar' => 'application/x-ustar', + 'vcd' => 'application/x-cdlink', + 'vda' => 'application/vda', + 'xlc' => 'application/vnd.ms-excel', + 'xll' => 'application/vnd.ms-excel', + 'xlm' => 'application/vnd.ms-excel', + 'xls' => 'application/vnd.ms-excel', + 'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'xlw' => 'application/vnd.ms-excel', + 'zip' => 'application/zip', + 'aif' => 'audio/x-aiff', + 'aifc' => 'audio/x-aiff', + 'aiff' => 'audio/x-aiff', + 'au' => 'audio/basic', + 'kar' => 'audio/midi', + 'mid' => 'audio/midi', + 'midi' => 'audio/midi', + 'mp2' => 'audio/mpeg', + 'mp3' => 'audio/mpeg', + 'mpga' => 'audio/mpeg', + 'ogg' => 'audio/ogg', + 'oga' => 'audio/ogg', + 'spx' => 'audio/ogg', + 'ra' => 'audio/x-realaudio', + 'ram' => 'audio/x-pn-realaudio', + 'rm' => 'audio/x-pn-realaudio', + 'rpm' => 'audio/x-pn-realaudio-plugin', + 'snd' => 'audio/basic', + 'tsi' => 'audio/TSP-audio', + 'wav' => 'audio/x-wav', + 'aac' => 'audio/aac', + 'asc' => 'text/plain', + 'c' => 'text/plain', + 'cc' => 'text/plain', + 'css' => 'text/css', + 'etx' => 'text/x-setext', + 'f' => 'text/plain', + 'f90' => 'text/plain', + 'h' => 'text/plain', + 'hh' => 'text/plain', + 'htm' => ['text/html', '*/*'], + 'ics' => 'text/calendar', + 'm' => 'text/plain', + 'rtf' => 'text/rtf', + 'rtx' => 'text/richtext', + 'sgm' => 'text/sgml', + 'sgml' => 'text/sgml', + 'tsv' => 'text/tab-separated-values', + 'tpl' => 'text/template', + 'txt' => 'text/plain', + 'text' => 'text/plain', + 'avi' => 'video/x-msvideo', + 'fli' => 'video/x-fli', + 'mov' => 'video/quicktime', + 'movie' => 'video/x-sgi-movie', + 'mpe' => 'video/mpeg', + 'mpeg' => 'video/mpeg', + 'mpg' => 'video/mpeg', + 'qt' => 'video/quicktime', + 'viv' => 'video/vnd.vivo', + 'vivo' => 'video/vnd.vivo', + 'ogv' => 'video/ogg', + 'webm' => 'video/webm', + 'mp4' => 'video/mp4', + 'm4v' => 'video/mp4', + 'f4v' => 'video/mp4', + 'f4p' => 'video/mp4', + 'm4a' => 'audio/mp4', + 'f4a' => 'audio/mp4', + 'f4b' => 'audio/mp4', + 'gif' => 'image/gif', + 'ief' => 'image/ief', + 'jpg' => 'image/jpeg', + 'jpeg' => 'image/jpeg', + 'jpe' => 'image/jpeg', + 'pbm' => 'image/x-portable-bitmap', + 'pgm' => 'image/x-portable-graymap', + 'png' => 'image/png', + 'pnm' => 'image/x-portable-anymap', + 'ppm' => 'image/x-portable-pixmap', + 'ras' => 'image/cmu-raster', + 'rgb' => 'image/x-rgb', + 'tif' => 'image/tiff', + 'tiff' => 'image/tiff', + 'xbm' => 'image/x-xbitmap', + 'xpm' => 'image/x-xpixmap', + 'xwd' => 'image/x-xwindowdump', + 'psd' => [ + 'application/photoshop', + 'application/psd', + 'image/psd', + 'image/x-photoshop', + 'image/photoshop', + 'zz-application/zz-winassoc-psd' + ], + 'ice' => 'x-conference/x-cooltalk', + 'iges' => 'model/iges', + 'igs' => 'model/iges', + 'mesh' => 'model/mesh', + 'msh' => 'model/mesh', + 'silo' => 'model/mesh', + 'vrml' => 'model/vrml', + 'wrl' => 'model/vrml', + 'mime' => 'www/mime', + 'pdb' => 'chemical/x-pdb', + 'xyz' => 'chemical/x-pdb', + 'javascript' => 'application/javascript', + 'form' => 'application/x-www-form-urlencoded', + 'file' => 'multipart/form-data', + 'xhtml' => ['application/xhtml+xml', 'application/xhtml', 'text/xhtml'], + 'xhtml-mobile' => 'application/vnd.wap.xhtml+xml', + 'atom' => 'application/atom+xml', + 'amf' => 'application/x-amf', + 'wap' => ['text/vnd.wap.wml', 'text/vnd.wap.wmlscript', 'image/vnd.wap.wbmp'], + 'wml' => 'text/vnd.wap.wml', + 'wmlscript' => 'text/vnd.wap.wmlscript', + 'wbmp' => 'image/vnd.wap.wbmp', + 'woff' => 'application/x-font-woff', + 'webp' => 'image/webp', + 'appcache' => 'text/cache-manifest', + 'manifest' => 'text/cache-manifest', + 'htc' => 'text/x-component', + 'rdf' => 'application/xml', + 'crx' => 'application/x-chrome-extension', + 'oex' => 'application/x-opera-extension', + 'xpi' => 'application/x-xpinstall', + 'safariextz' => 'application/octet-stream', + 'webapp' => 'application/x-web-app-manifest+json', + 'vcf' => 'text/x-vcard', + 'vtt' => 'text/vtt', + 'mkv' => 'video/x-matroska', + 'pkpass' => 'application/vnd.apple.pkpass', + 'ajax' => 'text/html' + ]; + + /** + * Protocol header to send to the client + * + * @var string + */ + protected $_protocol = 'HTTP/1.1'; + + /** + * Status code to send to the client + * + * @var int + */ + protected $_status = 200; + + /** + * Content type to send. This can be an 'extension' that will be transformed using the $_mimetypes array + * or a complete mime-type + * + * @var string + */ + protected $_contentType = 'text/html'; + + /** + * Buffer list of headers + * + * @var array + */ + protected $_headers = []; + + /** + * Buffer string for response message + * + * @var string + */ + protected $_body = null; + + /** + * File object for file to be read out as response + * + * @var File + */ + protected $_file = null; + + /** + * File range. Used for requesting ranges of files. + * + * @var array + */ + protected $_fileRange = null; + + /** + * The charset the response body is encoded with + * + * @var string + */ + protected $_charset = 'UTF-8'; + + /** + * Holds all the cache directives that will be converted + * into headers when sending the request + * + * @var array + */ + protected $_cacheDirectives = []; + + /** + * Holds cookies to be sent to the client + * + * @var array + */ + protected $_cookies = []; + + /** + * Constructor + * + * @param array $options list of parameters to setup the response. Possible values are: + * - body: the response text that should be sent to the client + * - statusCodes: additional allowable response codes + * - status: the HTTP status code to respond with + * - type: a complete mime-type string or an extension mapped in this class + * - charset: the charset for the response body + */ + public function __construct(array $options = []) + { + if (isset($options['body'])) { + $this->body($options['body']); + } + if (isset($options['statusCodes'])) { + $this->httpCodes($options['statusCodes']); + } + if (isset($options['status'])) { + $this->statusCode($options['status']); + } + if (isset($options['type'])) { + $this->type($options['type']); + } + if (!isset($options['charset'])) { + $options['charset'] = Configure::read('App.encoding'); + } + $this->charset($options['charset']); + } + + /** + * Buffers the response message to be sent + * if $content is null the current buffer is returned + * + * @param string $content the string message to be sent + * @return string current message buffer if $content param is passed as null + */ + public function body($content = null) + { + if ($content === null) { + return $this->_body; + } + return $this->_body = $content; + } + + /** + * Queries & sets valid HTTP response codes & messages. + * + * @param int|array $code If $code is an integer, then the corresponding code/message is + * returned if it exists, null if it does not exist. If $code is an array, then the + * keys are used as codes and the values as messages to add to the default HTTP + * codes. The codes must be integers greater than 99 and less than 1000. Keep in + * mind that the HTTP specification outlines that status codes begin with a digit + * between 1 and 5, which defines the class of response the client is to expect. + * Example: + * + * httpCodes(404); // returns array(404 => 'Not Found') + * + * httpCodes(array( + * 381 => 'Unicorn Moved', + * 555 => 'Unexpected Minotaur' + * )); // sets these new values, and returns true + * + * httpCodes(array( + * 0 => 'Nothing Here', + * -1 => 'Reverse Infinity', + * 12345 => 'Universal Password', + * 'Hello' => 'World' + * )); // throws an exception due to invalid codes + * + * For more on HTTP status codes see: http://www.w3.org/Protocols/rfc2616/rfc2616-sec6.html#sec6.1 + * + * @return array|null|true associative array of the HTTP codes as keys, and the message + * strings as values, or null of the given $code does not exist. `true` if `$code` is + * an array of valid codes. + * @throws CakeException If an attempt is made to add an invalid status code + */ + public function httpCodes($code = null) + { + if (empty($code)) { + return $this->_statusCodes; + } + if (is_array($code)) { + $codes = array_keys($code); + $min = min($codes); + if (!is_int($min) || $min < 100 || max($codes) > 999) { + throw new CakeException(__d('cake_dev', 'Invalid status code')); + } + $this->_statusCodes = $code + $this->_statusCodes; + return true; + } + if (!isset($this->_statusCodes[$code])) { + return null; + } + return [$code => $this->_statusCodes[$code]]; + } + + /** + * Sets the HTTP status code to be sent + * if $code is null the current code is returned + * + * @param int $code the HTTP status code + * @return int current status code + * @throws CakeException When an unknown status code is reached. + */ + public function statusCode($code = null) + { + if ($code === null) { + return $this->_status; + } + if (!isset($this->_statusCodes[$code])) { + throw new CakeException(__d('cake_dev', 'Unknown status code')); + } + return $this->_status = $code; + } + + /** + * Sets the response content type. It can be either a file extension + * which will be mapped internally to a mime-type or a string representing a mime-type + * if $contentType is null the current content type is returned + * if $contentType is an associative array, content type definitions will be stored/replaced + * + * ### Setting the content type + * + * e.g `type('jpg');` + * + * ### Returning the current content type + * + * e.g `type();` + * + * ### Storing content type definitions + * + * e.g `type(array('keynote' => 'application/keynote', 'bat' => 'application/bat'));` + * + * ### Replacing a content type definition + * + * e.g `type(array('jpg' => 'text/plain'));` + * + * @param array|string|null $contentType Content type key. + * @return string|false current content type or false if supplied an invalid content type + */ + public function type($contentType = null) + { + if ($contentType === null) { + return $this->_contentType; + } + if (is_array($contentType)) { + foreach ($contentType as $type => $definition) { + $this->_mimeTypes[$type] = $definition; + } + return $this->_contentType; + } + if (isset($this->_mimeTypes[$contentType])) { + $contentType = $this->_mimeTypes[$contentType]; + $contentType = is_array($contentType) ? current($contentType) : $contentType; + } + if (strpos($contentType, '/') === false) { + return false; + } + return $this->_contentType = $contentType; + } + + /** + * Sets the response charset + * if $charset is null the current charset is returned + * + * @param string $charset Character set string. + * @return string current charset + */ + public function charset($charset = null) + { + if ($charset === null) { + return $this->_charset; + } + return $this->_charset = $charset; + } + + /** + * Sends the complete response to the client including headers and message body. + * Will echo out the content in the response body. + * + * @return void + */ + public function send() + { + if (isset($this->_headers['Location']) && $this->_status === 200) { + $this->statusCode(302); + } + + $codeMessage = $this->_statusCodes[$this->_status]; + $this->_setCookies(); + $this->_sendHeader("{$this->_protocol} {$this->_status} {$codeMessage}"); + $this->_setContent(); + $this->_setContentLength(); + $this->_setContentType(); + foreach ($this->_headers as $header => $values) { + foreach ((array)$values as $value) { + $this->_sendHeader($header, $value); + } + } + if ($this->_file) { + $this->_sendFile($this->_file, $this->_fileRange); + $this->_file = $this->_fileRange = null; + } else { + $this->_sendContent($this->_body); + } + } + + /** + * Sets the cookies that have been added via CakeResponse::cookie() before any + * other output is sent to the client. Will set the cookies in the order they + * have been set. + * + * @return void + */ + protected function _setCookies() + { + foreach ($this->_cookies as $name => $c) { + setcookie( + $name, $c['value'], $c['expire'], $c['path'], + $c['domain'], $c['secure'], $c['httpOnly'] + ); + } + } + + /** + * Sends a header to the client. + * + * Will skip sending headers if headers have already been sent. + * + * @param string $name the header name + * @param string $value the header value + * @return void + */ + protected function _sendHeader($name, $value = null) + { + if (headers_sent($filename, $linenum)) { + return; + } + if ($value === null) { + header($name); + } else { + header("{$name}: {$value}"); + } + } + + /** + * Sets the response body to an empty text if the status code is 204 or 304 + * + * @return void + */ + protected function _setContent() + { + if (in_array($this->_status, [304, 204])) { + $this->body(''); + } + } + + /** + * Calculates the correct Content-Length and sets it as a header in the response + * Will not set the value if already set or if the output is compressed. + * + * @return void + */ + protected function _setContentLength() + { + $shouldSetLength = !isset($this->_headers['Content-Length']) && !in_array($this->_status, range(301, 307)); + if (isset($this->_headers['Content-Length']) && $this->_headers['Content-Length'] === false) { + unset($this->_headers['Content-Length']); + return; + } + if ($shouldSetLength && !$this->outputCompressed()) { + $offset = ob_get_level() ? ob_get_length() : 0; + if (ini_get('mbstring.func_overload') & 2 && function_exists('mb_strlen')) { + $this->length($offset + mb_strlen($this->_body, '8bit')); + } else { + $this->length($this->_headers['Content-Length'] = $offset + strlen($this->_body)); + } + } + } + + /** + * Returns whether the resulting output will be compressed by PHP + * + * @return bool + */ + public function outputCompressed() + { + return strpos(env('HTTP_ACCEPT_ENCODING'), 'gzip') !== false + && (ini_get("zlib.output_compression") === '1' || in_array('ob_gzhandler', ob_list_handlers())); + } + + /** + * Sets the Content-Length header for the response + * If called with no arguments returns the last Content-Length set + * + * @param int $bytes Number of bytes + * @return int|null + */ + public function length($bytes = null) + { + if ($bytes !== null) { + $this->_headers['Content-Length'] = $bytes; + } + if (isset($this->_headers['Content-Length'])) { + return $this->_headers['Content-Length']; + } + return null; + } + + /** + * Formats the Content-Type header based on the configured contentType and charset + * the charset will only be set in the header if the response is of type text + * + * @return void + */ + protected function _setContentType() + { + if (in_array($this->_status, [304, 204])) { + return; + } + $whitelist = [ + 'application/javascript', 'application/json', 'application/xml', 'application/rss+xml' + ]; + + $charset = false; + if ($this->_charset && + (strpos($this->_contentType, 'text/') === 0 || in_array($this->_contentType, $whitelist)) + ) { + $charset = true; + } + + if ($charset) { + $this->header('Content-Type', "{$this->_contentType}; charset={$this->_charset}"); + } else { + $this->header('Content-Type', "{$this->_contentType}"); + } + } + + /** + * Buffers a header string to be sent + * Returns the complete list of buffered headers + * + * ### Single header + * e.g `header('Location', 'http://example.com');` + * + * ### Multiple headers + * e.g `header(array('Location' => 'http://example.com', 'X-Extra' => 'My header'));` + * + * ### String header + * e.g `header('WWW-Authenticate: Negotiate');` + * + * ### Array of string headers + * e.g `header(array('WWW-Authenticate: Negotiate', 'Content-type: application/pdf'));` + * + * Multiple calls for setting the same header name will have the same effect as setting the header once + * with the last value sent for it + * e.g `header('WWW-Authenticate: Negotiate'); header('WWW-Authenticate: Not-Negotiate');` + * will have the same effect as only doing `header('WWW-Authenticate: Not-Negotiate');` + * + * @param string|array $header An array of header strings or a single header string + * - an associative array of "header name" => "header value" is also accepted + * - an array of string headers is also accepted + * @param string|array $value The header value(s) + * @return array list of headers to be sent + */ + public function header($header = null, $value = null) + { + if ($header === null) { + return $this->_headers; + } + $headers = is_array($header) ? $header : [$header => $value]; + foreach ($headers as $header => $value) { + if (is_numeric($header)) { + list($header, $value) = [$value, null]; + } + if ($value === null && strpos($header, ':') !== false) { + list($header, $value) = explode(':', $header, 2); + } + $this->_headers[$header] = is_array($value) ? array_map('trim', $value) : trim($value); + } + return $this->_headers; + } + + /** + * Reads out a file, and echos the content to the client. + * + * @param File $file File object + * @param array $range The range to read out of the file. + * @return bool True is whole file is echoed successfully or false if client connection is lost in between + */ + protected function _sendFile($file, $range) + { + $compress = $this->outputCompressed(); + $file->open('rb'); + + $end = $start = false; + if ($range && is_array($range)) { + list($start, $end) = $range; + } + if ($start !== false) { + $file->offset($start); + } + + $bufferSize = 8192; + set_time_limit(0); + session_write_close(); + while (!feof($file->handle)) { + if (!$this->_isActive()) { + $file->close(); + return false; + } + $offset = $file->offset(); + if ($end && $offset >= $end) { + break; + } + if ($end && $offset + $bufferSize >= $end) { + $bufferSize = $end - $offset + 1; + } + echo fread($file->handle, $bufferSize); + if (!$compress) { + $this->_flushBuffer(); + } + } + $file->close(); + return true; + } + + /** + * Returns true if connection is still active + * + * @return bool + */ + protected function _isActive() + { + return connection_status() === CONNECTION_NORMAL && !connection_aborted(); + } + + /** + * Flushes the contents of the output buffer + * + * @return void + */ + protected function _flushBuffer() + { + //@codingStandardsIgnoreStart + @flush(); + if (ob_get_level()) { + @ob_flush(); + } + //@codingStandardsIgnoreEnd + } + + /** + * Sends a content string to the client. + * + * @param string $content string to send as response body + * @return void + */ + protected function _sendContent($content) + { + echo $content; + } + + /** + * Accessor for the location header. + * + * Get/Set the Location header value. + * + * @param null|string $url Either null to get the current location, or a string to set one. + * @return string|null When setting the location null will be returned. When reading the location + * a string of the current location header value (if any) will be returned. + */ + public function location($url = null) + { + if ($url === null) { + $headers = $this->header(); + return isset($headers['Location']) ? $headers['Location'] : null; + } + $this->header('Location', $url); + return null; + } + + /** + * Returns the mime type definition for an alias + * + * e.g `getMimeType('pdf'); // returns 'application/pdf'` + * + * @param string $alias the content type alias to map + * @return mixed string mapped mime type or false if $alias is not mapped + */ + public function getMimeType($alias) + { + if (isset($this->_mimeTypes[$alias])) { + return $this->_mimeTypes[$alias]; + } + return false; + } + + /** + * Maps a content-type back to an alias + * + * e.g `mapType('application/pdf'); // returns 'pdf'` + * + * @param string|array $ctype Either a string content type to map, or an array of types. + * @return mixed Aliases for the types provided. + */ + public function mapType($ctype) + { + if (is_array($ctype)) { + return array_map([$this, 'mapType'], $ctype); + } + + foreach ($this->_mimeTypes as $alias => $types) { + if (in_array($ctype, (array)$types)) { + return $alias; + } + } + return null; + } + + /** + * Sets the correct headers to instruct the client to not cache the response + * + * @return void + */ + public function disableCache() + { + $this->header([ + 'Expires' => 'Mon, 26 Jul 1997 05:00:00 GMT', + 'Last-Modified' => gmdate("D, d M Y H:i:s") . " GMT", + 'Cache-Control' => 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0' + ]); + } + + /** + * Sets the correct headers to instruct the client to cache the response. + * + * @param string|int $since a valid time since the response text has not been modified + * @param string|int $time a valid time for cache expiry + * @return void + */ + public function cache($since, $time = '+1 day') + { + if (!is_int($time)) { + $time = strtotime($time); + } + $this->header([ + 'Date' => gmdate("D, j M Y G:i:s ", time()) . 'GMT' + ]); + $this->modified($since); + $this->expires($time); + $this->sharable(true); + $this->maxAge($time - time()); + } + + /** + * Sets the Last-Modified header for the response by taking a modification time + * If called with no parameters it will return the current Last-Modified value + * + * ## Examples: + * + * `$response->modified('now')` Will set the Last-Modified to the current time + * `$response->modified(new DateTime('+1 day'))` Will set the modification date in the past 24 hours + * `$response->modified()` Will return the current Last-Modified header value + * + * @param string|DateTime $time Valid time string or DateTime object. + * @return string + */ + public function modified($time = null) + { + if ($time !== null) { + $date = $this->_getUTCDate($time); + $this->_headers['Last-Modified'] = $date->format('D, j M Y H:i:s') . ' GMT'; + } + if (isset($this->_headers['Last-Modified'])) { + return $this->_headers['Last-Modified']; + } + return null; + } + + /** + * Returns a DateTime object initialized at the $time param and using UTC + * as timezone + * + * @param DateTime|int|string $time Valid time string or unix timestamp or DateTime object. + * @return DateTime + */ + protected function _getUTCDate($time = null) + { + if ($time instanceof DateTime) { + $result = clone $time; + } else if (is_int($time)) { + $result = new DateTime(date('Y-m-d H:i:s', $time)); + } else { + $result = new DateTime($time); + } + $result->setTimeZone(new DateTimeZone('UTC')); + return $result; + } + + /** + * Sets the Expires header for the response by taking an expiration time + * If called with no parameters it will return the current Expires value + * + * ## Examples: + * + * `$response->expires('now')` Will Expire the response cache now + * `$response->expires(new DateTime('+1 day'))` Will set the expiration in next 24 hours + * `$response->expires()` Will return the current expiration header value + * + * @param string|DateTime $time Valid time string or DateTime object. + * @return string + */ + public function expires($time = null) + { + if ($time !== null) { + $date = $this->_getUTCDate($time); + $this->_headers['Expires'] = $date->format('D, j M Y H:i:s') . ' GMT'; + } + if (isset($this->_headers['Expires'])) { + return $this->_headers['Expires']; + } + return null; + } + + /** + * Sets whether a response is eligible to be cached by intermediate proxies + * This method controls the `public` or `private` directive in the Cache-Control + * header + * + * @param bool $public If set to true, the Cache-Control header will be set as public + * if set to false, the response will be set to private + * if no value is provided, it will return whether the response is sharable or not + * @param int $time time in seconds after which the response should no longer be considered fresh + * @return bool + */ + public function sharable($public = null, $time = null) + { + if ($public === null) { + $public = array_key_exists('public', $this->_cacheDirectives); + $private = array_key_exists('private', $this->_cacheDirectives); + $noCache = array_key_exists('no-cache', $this->_cacheDirectives); + if (!$public && !$private && !$noCache) { + return null; + } + $sharable = $public || !($private || $noCache); + return $sharable; + } + if ($public) { + $this->_cacheDirectives['public'] = true; + unset($this->_cacheDirectives['private']); + } else { + $this->_cacheDirectives['private'] = true; + unset($this->_cacheDirectives['public']); + } + + $this->maxAge($time); + if ((int)$time === 0) { + $this->_setCacheControl(); + } + return (bool)$public; + } + + /** + * Sets the Cache-Control max-age directive. + * The max-age is the number of seconds after which the response should no longer be considered + * a good candidate to be fetched from the local (client) cache. + * If called with no parameters, this function will return the current max-age value if any + * + * @param int $seconds if null, the method will return the current max-age value + * @return int + */ + public function maxAge($seconds = null) + { + if ($seconds !== null) { + $this->_cacheDirectives['max-age'] = $seconds; + $this->_setCacheControl(); + } + if (isset($this->_cacheDirectives['max-age'])) { + return $this->_cacheDirectives['max-age']; + } + return null; + } + + /** + * Helper method to generate a valid Cache-Control header from the options set + * in other methods + * + * @return void + */ + protected function _setCacheControl() + { + $control = ''; + foreach ($this->_cacheDirectives as $key => $val) { + $control .= $val === true ? $key : sprintf('%s=%s', $key, $val); + $control .= ', '; + } + $control = rtrim($control, ', '); + $this->header('Cache-Control', $control); + } + + /** + * Sets the Cache-Control s-maxage directive. + * The max-age is the number of seconds after which the response should no longer be considered + * a good candidate to be fetched from a shared cache (like in a proxy server). + * If called with no parameters, this function will return the current max-age value if any + * + * @param int $seconds if null, the method will return the current s-maxage value + * @return int + */ + public function sharedMaxAge($seconds = null) + { + if ($seconds !== null) { + $this->_cacheDirectives['s-maxage'] = $seconds; + $this->_setCacheControl(); + } + if (isset($this->_cacheDirectives['s-maxage'])) { + return $this->_cacheDirectives['s-maxage']; + } + return null; + } + + /** + * Sets the Cache-Control must-revalidate directive. + * must-revalidate indicates that the response should not be served + * stale by a cache under any circumstance without first revalidating + * with the origin. + * If called with no parameters, this function will return whether must-revalidate is present. + * + * @param bool $enable If null returns whether directive is set, if boolean + * sets or unsets directive. + * @return bool + */ + public function mustRevalidate($enable = null) + { + if ($enable !== null) { + if ($enable) { + $this->_cacheDirectives['must-revalidate'] = true; + } else { + unset($this->_cacheDirectives['must-revalidate']); + } + $this->_setCacheControl(); + } + return array_key_exists('must-revalidate', $this->_cacheDirectives); + } + + /** + * Sets the Vary header for the response, if an array is passed, + * values will be imploded into a comma separated string. If no + * parameters are passed, then an array with the current Vary header + * value is returned + * + * @param string|array $cacheVariances a single Vary string or an array + * containing the list for variances. + * @return array + */ + public function vary($cacheVariances = null) + { + if ($cacheVariances !== null) { + $cacheVariances = (array)$cacheVariances; + $this->_headers['Vary'] = implode(', ', $cacheVariances); + } + if (isset($this->_headers['Vary'])) { + return explode(', ', $this->_headers['Vary']); + } + return null; + } + + /** + * Sets the correct output buffering handler to send a compressed response. Responses will + * be compressed with zlib, if the extension is available. + * + * @return bool false if client does not accept compressed responses or no handler is available, true otherwise + */ + public function compress() + { + $compressionEnabled = ini_get("zlib.output_compression") !== '1' && + extension_loaded("zlib") && + (strpos(env('HTTP_ACCEPT_ENCODING'), 'gzip') !== false); + return $compressionEnabled && ob_start('ob_gzhandler'); + } + + /** + * Sets the protocol to be used when sending the response. Defaults to HTTP/1.1 + * If called with no arguments, it will return the current configured protocol + * + * @param string $protocol Protocol to be used for sending response. + * @return string protocol currently set + */ + public function protocol($protocol = null) + { + if ($protocol !== null) { + $this->_protocol = $protocol; + } + return $this->_protocol; + } + + /** + * Checks whether a response has not been modified according to the 'If-None-Match' + * (Etags) and 'If-Modified-Since' (last modification date) request + * headers. If the response is detected to be not modified, it + * is marked as so accordingly so the client can be informed of that. + * + * In order to mark a response as not modified, you need to set at least + * the Last-Modified etag response header before calling this method. Otherwise + * a comparison will not be possible. + * + * @param CakeRequest $request Request object + * @return bool whether the response was marked as not modified or not. + */ + public function checkNotModified(CakeRequest $request) + { + $ifNoneMatchHeader = $request->header('If-None-Match'); + $etags = []; + if (is_string($ifNoneMatchHeader)) { + $etags = preg_split('/\s*,\s*/', $ifNoneMatchHeader, null, PREG_SPLIT_NO_EMPTY); + } + $modifiedSince = $request->header('If-Modified-Since'); + $checks = []; + if ($responseTag = $this->etag()) { + $checks[] = in_array('*', $etags) || in_array($responseTag, $etags); + } + if ($modifiedSince) { + $checks[] = strtotime($this->modified()) === strtotime($modifiedSince); + } + if (empty($checks)) { + return false; + } + $notModified = !in_array(false, $checks, true); + if ($notModified) { + $this->notModified(); + } + return $notModified; + } + + /** + * Sets the response Etag, Etags are a strong indicative that a response + * can be cached by a HTTP client. A bad way of generating Etags is + * creating a hash of the response output, instead generate a unique + * hash of the unique components that identifies a request, such as a + * modification time, a resource Id, and anything else you consider it + * makes it unique. + * + * Second parameter is used to instruct clients that the content has + * changed, but sematicallly, it can be used as the same thing. Think + * for instance of a page with a hit counter, two different page views + * are equivalent, but they differ by a few bytes. This leaves off to + * the Client the decision of using or not the cached page. + * + * If no parameters are passed, current Etag header is returned. + * + * @param string $tag Tag to set. + * @param bool $weak whether the response is semantically the same as + * other with the same hash or not + * @return string + */ + public function etag($tag = null, $weak = false) + { + if ($tag !== null) { + $this->_headers['Etag'] = sprintf('%s"%s"', ($weak) ? 'W/' : null, $tag); + } + if (isset($this->_headers['Etag'])) { + return $this->_headers['Etag']; + } + return null; + } + + /** + * Sets the response as Not Modified by removing any body contents + * setting the status code to "304 Not Modified" and removing all + * conflicting headers + * + * @return void + */ + public function notModified() + { + $this->statusCode(304); + $this->body(''); + $remove = [ + 'Allow', + 'Content-Encoding', + 'Content-Language', + 'Content-Length', + 'Content-MD5', + 'Content-Type', + 'Last-Modified' + ]; + foreach ($remove as $header) { + unset($this->_headers[$header]); + } + } + + /** + * String conversion. Fetches the response body as a string. + * Does *not* send headers. + * + * @return string + */ + public function __toString() + { + return (string)$this->_body; + } + + /** + * Getter/Setter for cookie configs + * + * This method acts as a setter/getter depending on the type of the argument. + * If the method is called with no arguments, it returns all configurations. + * + * If the method is called with a string as argument, it returns either the + * given configuration if it is set, or null, if it's not set. + * + * If the method is called with an array as argument, it will set the cookie + * configuration to the cookie container. + * + * ### Options (when setting a configuration) + * - name: The Cookie name + * - value: Value of the cookie + * - expire: Time the cookie expires in + * - path: Path the cookie applies to + * - domain: Domain the cookie is for. + * - secure: Is the cookie https? + * - httpOnly: Is the cookie available in the client? + * + * ## Examples + * + * ### Getting all cookies + * + * `$this->cookie()` + * + * ### Getting a certain cookie configuration + * + * `$this->cookie('MyCookie')` + * + * ### Setting a cookie configuration + * + * `$this->cookie((array) $options)` + * + * @param array|string $options Either null to get all cookies, string for a specific cookie + * or array to set cookie. + * @return mixed + */ + public function cookie($options = null) + { + if ($options === null) { + return $this->_cookies; + } + + if (is_string($options)) { + if (!isset($this->_cookies[$options])) { + return null; + } + return $this->_cookies[$options]; + } + + $defaults = [ + 'name' => 'CakeCookie[default]', + 'value' => '', + 'expire' => 0, + 'path' => '/', + 'domain' => '', + 'secure' => false, + 'httpOnly' => false + ]; + $options += $defaults; + + $this->_cookies[$options['name']] = $options; + } + + /** + * Setup access for origin and methods on cross origin requests + * + * This method allow multiple ways to setup the domains, see the examples + * + * ### Full URI + * e.g `cors($request, 'https://www.cakephp.org');` + * + * ### URI with wildcard + * e.g `cors($request, 'http://*.cakephp.org');` + * + * ### Ignoring the requested protocol + * e.g `cors($request, 'www.cakephp.org');` + * + * ### Any URI + * e.g `cors($request, '*');` + * + * ### Whitelist of URIs + * e.g `cors($request, array('https://www.cakephp.org', '*.google.com', 'https://myproject.github.io'));` + * + * @param CakeRequest $request Request object + * @param string|array $allowedDomains List of allowed domains, see method description for more details + * @param string|array $allowedMethods List of HTTP verbs allowed + * @param string|array $allowedHeaders List of HTTP headers allowed + * @return void + */ + public function cors(CakeRequest $request, $allowedDomains, $allowedMethods = [], $allowedHeaders = []) + { + $origin = $request->header('Origin'); + if (!$origin) { + return; + } + + $allowedDomains = $this->_normalizeCorsDomains((array)$allowedDomains, $request->is('ssl')); + foreach ($allowedDomains as $domain) { + if (!preg_match($domain['preg'], $origin)) { + continue; + } + $this->header('Access-Control-Allow-Origin', $domain['original'] === '*' ? '*' : $origin); + $allowedMethods && $this->header('Access-Control-Allow-Methods', implode(', ', (array)$allowedMethods)); + $allowedHeaders && $this->header('Access-Control-Allow-Headers', implode(', ', (array)$allowedHeaders)); + break; + } + } + + /** + * Normalize the origin to regular expressions and put in an array format + * + * @param array $domains Domains to normalize + * @param bool $requestIsSSL Whether it's a SSL request. + * @return array + */ + protected function _normalizeCorsDomains($domains, $requestIsSSL = false) + { + $result = []; + foreach ($domains as $domain) { + if ($domain === '*') { + $result[] = ['preg' => '@.@', 'original' => '*']; + continue; + } + $original = $domain; + $preg = '@' . str_replace('*', '.*', $domain) . '@'; + $result[] = compact('original', 'preg'); + } + return $result; + } + + /** + * Setup for display or download the given file. + * + * If $_SERVER['HTTP_RANGE'] is set a slice of the file will be + * returned instead of the entire file. + * + * ### Options keys + * + * - name: Alternate download name + * - download: If `true` sets download header and forces file to be downloaded rather than displayed in browser + * + * @param string $path Path to file. If the path is not an absolute path that resolves + * to a file, `APP` will be prepended to the path. + * @param array $options Options See above. + * @return void + * @throws NotFoundException + */ + public function file($path, $options = []) + { + $options += [ + 'name' => null, + 'download' => null + ]; + + if (strpos($path, '../') !== false || strpos($path, '..\\') !== false) { + throw new NotFoundException(__d( + 'cake_dev', + 'The requested file contains `..` and will not be read.' + )); + } + + if (!is_file($path)) { + $path = APP . $path; + } + + $file = new File($path); + if (!$file->exists() || !$file->readable()) { + if (Configure::read('debug')) { + throw new NotFoundException(__d('cake_dev', 'The requested file %s was not found or not readable', $path)); + } + throw new NotFoundException(__d('cake', 'The requested file was not found')); + } + + $extension = strtolower($file->ext()); + $download = $options['download']; + if ((!$extension || $this->type($extension) === false) && $download === null) { + $download = true; + } + + $fileSize = $file->size(); + if ($download) { + $agent = env('HTTP_USER_AGENT'); + + if (preg_match('%Opera(/| )([0-9].[0-9]{1,2})%', $agent)) { + $contentType = 'application/octet-stream'; + } else if (preg_match('/MSIE ([0-9].[0-9]{1,2})/', $agent)) { + $contentType = 'application/force-download'; + } + + if (!empty($contentType)) { + $this->type($contentType); + } + if ($options['name'] === null) { + $name = $file->name; + } else { + $name = $options['name']; + } + $this->download($name); + $this->header('Content-Transfer-Encoding', 'binary'); + } + + $this->header('Accept-Ranges', 'bytes'); + $httpRange = env('HTTP_RANGE'); + if (isset($httpRange)) { + $this->_fileRange($file, $httpRange); + } else { + $this->header('Content-Length', $fileSize); + } + + $this->_clearBuffer(); + $this->_file = $file; + } + + /** + * Sets the correct headers to instruct the browser to download the response as a file. + * + * @param string $filename the name of the file as the browser will download the response + * @return void + */ + public function download($filename) + { + $this->header('Content-Disposition', 'attachment; filename="' . $filename . '"'); + } + + /** + * Apply a file range to a file and set the end offset. + * + * If an invalid range is requested a 416 Status code will be used + * in the response. + * + * @param File $file The file to set a range on. + * @param string $httpRange The range to use. + * @return void + */ + protected function _fileRange($file, $httpRange) + { + $fileSize = $file->size(); + $lastByte = $fileSize - 1; + $start = 0; + $end = $lastByte; + + preg_match('/^bytes\s*=\s*(\d+)?\s*-\s*(\d+)?$/', $httpRange, $matches); + if ($matches) { + $start = $matches[1]; + $end = isset($matches[2]) ? $matches[2] : ''; + } + + if ($start === '') { + $start = $fileSize - $end; + $end = $lastByte; + } + if ($end === '') { + $end = $lastByte; + } + + if ($start > $end || $end > $lastByte || $start > $lastByte) { + $this->statusCode(416); + $this->header([ + 'Content-Range' => 'bytes 0-' . $lastByte . '/' . $fileSize + ]); + return; + } + + $this->header([ + 'Content-Length' => $end - $start + 1, + 'Content-Range' => 'bytes ' . $start . '-' . $end . '/' . $fileSize + ]); + + $this->statusCode(206); + $this->_fileRange = [$start, $end]; + } + + /** + * Clears the contents of the topmost output buffer and discards them + * + * @return bool + */ + protected function _clearBuffer() + { + if (ob_get_length()) { + return ob_end_clean(); + } + return true; + } } diff --git a/lib/Cake/Network/CakeSocket.php b/lib/Cake/Network/CakeSocket.php index 74799825..4f599130 100755 --- a/lib/Cake/Network/CakeSocket.php +++ b/lib/Cake/Network/CakeSocket.php @@ -9,11 +9,11 @@ * For full copyright and license information, please see the LICENSE.txt * Redistributions of files must retain the above copyright notice. * - * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org) - * @link https://cakephp.org CakePHP(tm) Project - * @package Cake.Network - * @since CakePHP(tm) v 1.2.0 - * @license https://opensource.org/licenses/mit-license.php MIT License + * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org) + * @link https://cakephp.org CakePHP(tm) Project + * @package Cake.Network + * @since CakePHP(tm) v 1.2.0 + * @license https://opensource.org/licenses/mit-license.php MIT License */ App::uses('Validation', 'Utility'); @@ -23,9 +23,10 @@ * * Core base class for network communication. * - * @package Cake.Network + * @package Cake.Network */ -class CakeSocket { +class CakeSocket +{ /** * CakeSocket description @@ -33,62 +34,55 @@ class CakeSocket { * @var string */ public $description = 'Remote DataSource Network Socket Interface'; - - /** - * Base configuration settings for the socket connection - * - * @var array - */ - protected $_baseConfig = array( - 'persistent' => false, - 'host' => 'localhost', - 'protocol' => 'tcp', - 'port' => 80, - 'timeout' => 30, - 'cryptoType' => 'tls', - ); - /** * Configuration settings for the socket connection * * @var array */ - public $config = array(); - + public $config = []; /** * Reference to socket connection resource * * @var resource */ public $connection = null; - /** * This boolean contains the current state of the CakeSocket class * * @var bool */ public $connected = false; - /** * This variable contains an array with the last error number (num) and string (str) * * @var array */ - public $lastError = array(); - + public $lastError = []; /** * True if the socket stream is encrypted after a CakeSocket::enableCrypto() call * * @var bool */ public $encrypted = false; - + /** + * Base configuration settings for the socket connection + * + * @var array + */ + protected $_baseConfig = [ + 'persistent' => false, + 'host' => 'localhost', + 'protocol' => 'tcp', + 'port' => 80, + 'timeout' => 30, + 'cryptoType' => 'tls', + ]; /** * Contains all the encryption methods available * * @var array */ - protected $_encryptMethods = array( + protected $_encryptMethods = [ // @codingStandardsIgnoreStart 'sslv2_client' => STREAM_CRYPTO_METHOD_SSLv2_CLIENT, 'sslv3_client' => STREAM_CRYPTO_METHOD_SSLv3_CLIENT, @@ -99,7 +93,7 @@ class CakeSocket { 'sslv23_server' => STREAM_CRYPTO_METHOD_SSLv23_SERVER, 'tls_server' => STREAM_CRYPTO_METHOD_TLS_SERVER, // @codingStandardsIgnoreEnd - ); + ]; /** * Used to capture connection warnings which can happen when there are @@ -107,7 +101,7 @@ class CakeSocket { * * @var array */ - protected $_connectionErrors = array(); + protected $_connectionErrors = []; /** * Constructor. @@ -115,7 +109,8 @@ class CakeSocket { * @param array $config Socket configuration, which will be merged with the base configuration * @see CakeSocket::$_baseConfig */ - public function __construct($config = array()) { + public function __construct($config = []) + { $this->config = array_merge($this->_baseConfig, $config); $this->_addTlsVersions(); @@ -134,15 +129,16 @@ public function __construct($config = array()) { * @see https://github.com/php/php-src/commit/10bc5fd4c4c8e1dd57bd911b086e9872a56300a0 * @return void */ - protected function _addTlsVersions() { - $conditionalCrypto = array( + protected function _addTlsVersions() + { + $conditionalCrypto = [ 'tlsv1_1_client' => 'STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT', 'tlsv1_2_client' => 'STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT', 'tlsv1_1_server' => 'STREAM_CRYPTO_METHOD_TLSv1_1_SERVER', 'tlsv1_2_server' => 'STREAM_CRYPTO_METHOD_TLSv1_2_SERVER', 'tlsv1_3_server' => 'STREAM_CRYPTO_METHOD_TLSv1_3_SERVER', 'tlsv1_3_client' => 'STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT' - ); + ]; foreach ($conditionalCrypto as $key => $const) { if (defined($const)) { $this->_encryptMethods[$key] = constant($const); @@ -171,13 +167,102 @@ protected function _addTlsVersions() { // @codingStandardsIgnoreEnd } + /** + * Gets the connection context. + * + * @return null|array Null when there is no connection, an array when there is. + */ + public function context() + { + if (!$this->connection) { + return null; + } + return stream_context_get_options($this->connection); + } + + /** + * Gets the host name of the current connection. + * + * @return string Host name + */ + public function host() + { + if (Validation::ip($this->config['host'])) { + return gethostbyaddr($this->config['host']); + } + return gethostbyaddr($this->address()); + } + + /** + * Gets the IP address of the current connection. + * + * @return string IP address + */ + public function address() + { + if (Validation::ip($this->config['host'])) { + return $this->config['host']; + } + return gethostbyname($this->config['host']); + } + + /** + * Gets all IP addresses associated with the current connection. + * + * @return array IP addresses + */ + public function addresses() + { + if (Validation::ip($this->config['host'])) { + return [$this->config['host']]; + } + return gethostbynamel($this->config['host']); + } + + /** + * Gets the last error as a string. + * + * @return string|null Last error + */ + public function lastError() + { + if (!empty($this->lastError)) { + return $this->lastError['num'] . ': ' . $this->lastError['str']; + } + return null; + } + + /** + * Writes data to the socket. + * + * @param string $data The data to write to the socket + * @return bool Success + */ + public function write($data) + { + if (!$this->connected) { + if (!$this->connect()) { + return false; + } + } + $totalBytes = strlen($data); + for ($written = 0, $rv = 0; $written < $totalBytes; $written += $rv) { + $rv = fwrite($this->connection, substr($data, $written)); + if ($rv === false || $rv === 0) { + return $written; + } + } + return $written; + } + /** * Connects the socket to the given host and port. * * @return bool Success * @throws SocketException */ - public function connect() { + public function connect() + { if ($this->connection) { $this->disconnect(); } @@ -211,7 +296,7 @@ public function connect() { $connectAs |= STREAM_CLIENT_PERSISTENT; } - set_error_handler(array($this, '_connectionErrorHandler')); + set_error_handler([$this, '_connectionErrorHandler']); $this->connection = stream_socket_client( $scheme . $this->config['host'] . ':' . $this->config['port'], $errNum, @@ -240,7 +325,7 @@ public function connect() { $this->config['request']['uri']['scheme'] === 'https' && !empty($this->config['proxy']) ) { - $req = array(); + $req = []; $req[] = 'CONNECT ' . $this->config['request']['uri']['host'] . ':' . $this->config['request']['uri']['port'] . ' HTTP/1.1'; $req[] = 'Host: ' . $this->config['host']; @@ -264,13 +349,33 @@ public function connect() { return $this->connected; } + /** + * Disconnects the socket from the current connection. + * + * @return bool Success + */ + public function disconnect() + { + if (!is_resource($this->connection)) { + $this->connected = false; + return true; + } + $this->connected = !fclose($this->connection); + + if (!$this->connected) { + $this->connection = null; + } + return !$this->connected; + } + /** * Configure the SSL context options. * * @param string $host The host name being connected to. * @return void */ - protected function _setSslContext($host) { + protected function _setSslContext($host) + { foreach ($this->config as $key => $value) { if (substr($key, 0, 4) !== 'ssl_') { continue; @@ -304,80 +409,6 @@ protected function _setSslContext($host) { unset($this->config['context']['ssl']['verify_host']); } - /** - * socket_stream_client() does not populate errNum, or $errStr when there are - * connection errors, as in the case of SSL verification failure. - * - * Instead we need to handle those errors manually. - * - * @param int $code Code. - * @param string $message Message. - * @return void - */ - protected function _connectionErrorHandler($code, $message) { - $this->_connectionErrors[] = $message; - } - - /** - * Gets the connection context. - * - * @return null|array Null when there is no connection, an array when there is. - */ - public function context() { - if (!$this->connection) { - return null; - } - return stream_context_get_options($this->connection); - } - - /** - * Gets the host name of the current connection. - * - * @return string Host name - */ - public function host() { - if (Validation::ip($this->config['host'])) { - return gethostbyaddr($this->config['host']); - } - return gethostbyaddr($this->address()); - } - - /** - * Gets the IP address of the current connection. - * - * @return string IP address - */ - public function address() { - if (Validation::ip($this->config['host'])) { - return $this->config['host']; - } - return gethostbyname($this->config['host']); - } - - /** - * Gets all IP addresses associated with the current connection. - * - * @return array IP addresses - */ - public function addresses() { - if (Validation::ip($this->config['host'])) { - return array($this->config['host']); - } - return gethostbynamel($this->config['host']); - } - - /** - * Gets the last error as a string. - * - * @return string|null Last error - */ - public function lastError() { - if (!empty($this->lastError)) { - return $this->lastError['num'] . ': ' . $this->lastError['str']; - } - return null; - } - /** * Sets the last error. * @@ -385,30 +416,42 @@ public function lastError() { * @param string $errStr Error string * @return void */ - public function setLastError($errNum, $errStr) { - $this->lastError = array('num' => $errNum, 'str' => $errStr); + public function setLastError($errNum, $errStr) + { + $this->lastError = ['num' => $errNum, 'str' => $errStr]; } /** - * Writes data to the socket. + * Encrypts current stream socket, using one of the defined encryption methods. * - * @param string $data The data to write to the socket - * @return bool Success + * @param string $type Type which can be one of 'sslv2', 'sslv3', 'sslv23', 'tls', 'tlsv1_1' or 'tlsv1_2'. + * @param string $clientOrServer Can be one of 'client', 'server'. Default is 'client'. + * @param bool $enable Enable or disable encryption. Default is true (enable) + * @return bool True on success + * @throws InvalidArgumentException When an invalid encryption scheme is chosen. + * @throws SocketException When attempting to enable SSL/TLS fails. + * @see stream_socket_enable_crypto */ - public function write($data) { - if (!$this->connected) { - if (!$this->connect()) { - return false; - } + public function enableCrypto($type, $clientOrServer = 'client', $enable = true) + { + if (!array_key_exists($type . '_' . $clientOrServer, $this->_encryptMethods)) { + throw new InvalidArgumentException(__d('cake_dev', 'Invalid encryption scheme chosen')); } - $totalBytes = strlen($data); - for ($written = 0, $rv = 0; $written < $totalBytes; $written += $rv) { - $rv = fwrite($this->connection, substr($data, $written)); - if ($rv === false || $rv === 0) { - return $written; - } + $enableCryptoResult = false; + try { + $enableCryptoResult = stream_socket_enable_crypto($this->connection, $enable, + $this->_encryptMethods[$type . '_' . $clientOrServer]); + } catch (Exception $e) { + $this->setLastError(null, $e->getMessage()); + throw new SocketException($e->getMessage()); } - return $written; + if ($enableCryptoResult === true) { + $this->encrypted = $enable; + return true; + } + $errorMessage = __d('cake_dev', 'Unable to perform enableCrypto operation on CakeSocket'); + $this->setLastError(null, $errorMessage); + throw new SocketException($errorMessage); } /** @@ -418,7 +461,8 @@ public function write($data) { * @param int $length Optional buffer length to read; defaults to 1024 * @return mixed Socket data */ - public function read($length = 1024) { + public function read($length = 1024) + { if (!$this->connected) { if (!$this->connect()) { return false; @@ -437,28 +481,11 @@ public function read($length = 1024) { return false; } - /** - * Disconnects the socket from the current connection. - * - * @return bool Success - */ - public function disconnect() { - if (!is_resource($this->connection)) { - $this->connected = false; - return true; - } - $this->connected = !fclose($this->connection); - - if (!$this->connected) { - $this->connection = null; - } - return !$this->connected; - } - /** * Destructor, used to disconnect from current connection. */ - public function __destruct() { + public function __destruct() + { $this->disconnect(); } @@ -468,9 +495,10 @@ public function __destruct() { * @param array $state Array with key and values to reset * @return bool True on success */ - public function reset($state = null) { + public function reset($state = null) + { if (empty($state)) { - static $initalState = array(); + static $initalState = []; if (empty($initalState)) { $initalState = get_class_vars(__CLASS__); } @@ -484,34 +512,17 @@ public function reset($state = null) { } /** - * Encrypts current stream socket, using one of the defined encryption methods. + * socket_stream_client() does not populate errNum, or $errStr when there are + * connection errors, as in the case of SSL verification failure. * - * @param string $type Type which can be one of 'sslv2', 'sslv3', 'sslv23', 'tls', 'tlsv1_1' or 'tlsv1_2'. - * @param string $clientOrServer Can be one of 'client', 'server'. Default is 'client'. - * @param bool $enable Enable or disable encryption. Default is true (enable) - * @return bool True on success - * @throws InvalidArgumentException When an invalid encryption scheme is chosen. - * @throws SocketException When attempting to enable SSL/TLS fails. - * @see stream_socket_enable_crypto + * Instead we need to handle those errors manually. + * + * @param int $code Code. + * @param string $message Message. + * @return void */ - public function enableCrypto($type, $clientOrServer = 'client', $enable = true) { - if (!array_key_exists($type . '_' . $clientOrServer, $this->_encryptMethods)) { - throw new InvalidArgumentException(__d('cake_dev', 'Invalid encryption scheme chosen')); - } - $enableCryptoResult = false; - try { - $enableCryptoResult = stream_socket_enable_crypto($this->connection, $enable, - $this->_encryptMethods[$type . '_' . $clientOrServer]); - } catch (Exception $e) { - $this->setLastError(null, $e->getMessage()); - throw new SocketException($e->getMessage()); - } - if ($enableCryptoResult === true) { - $this->encrypted = $enable; - return true; - } - $errorMessage = __d('cake_dev', 'Unable to perform enableCrypto operation on CakeSocket'); - $this->setLastError(null, $errorMessage); - throw new SocketException($errorMessage); + protected function _connectionErrorHandler($code, $message) + { + $this->_connectionErrors[] = $message; } } \ No newline at end of file diff --git a/lib/Cake/Network/Email/AbstractTransport.php b/lib/Cake/Network/Email/AbstractTransport.php index 453b7436..c6777687 100755 --- a/lib/Cake/Network/Email/AbstractTransport.php +++ b/lib/Cake/Network/Email/AbstractTransport.php @@ -21,55 +21,58 @@ * * @package Cake.Network.Email */ -abstract class AbstractTransport { +abstract class AbstractTransport +{ -/** - * Configurations - * - * @var array - */ - protected $_config = array(); + /** + * Configurations + * + * @var array + */ + protected $_config = []; -/** - * Send mail - * - * @param CakeEmail $email CakeEmail instance. - * @return array - */ - abstract public function send(CakeEmail $email); + /** + * Send mail + * + * @param CakeEmail $email CakeEmail instance. + * @return array + */ + abstract public function send(CakeEmail $email); -/** - * Set the config - * - * @param array $config Configuration options. - * @return array Returns configs - */ - public function config($config = null) { - if (is_array($config)) { - $this->_config = $config + $this->_config; - } - return $this->_config; - } + /** + * Set the config + * + * @param array $config Configuration options. + * @return array Returns configs + */ + public function config($config = null) + { + if (is_array($config)) { + $this->_config = $config + $this->_config; + } + return $this->_config; + } -/** - * Help to convert headers in string - * - * @param array $headers Headers in format key => value - * @param string $eol End of line string. - * @return string - */ - protected function _headersToString($headers, $eol = "\r\n") { - $out = ''; - foreach ($headers as $key => $value) { - if ($value === false || $value === null || $value === '') { - continue; - } - $out .= $key . ': ' . $value . $eol; - } - if (!empty($out)) { - $out = substr($out, 0, -1 * strlen($eol)); - } - return $out; - } + /** + * Help to convert headers in string + * + * @param array $headers Headers in format key => value + * @param string $eol End of line string. + * @return string + */ + protected function _headersToString($headers, $eol = "\r\n") + { + $out = ''; + foreach ($headers as $key => $value) { + if ($value === false || $value === null || $value === '') { + continue; + } + $out .= $key . ': ' . $value . $eol; + } + if (!empty($out)) { + $out = substr($out, 0, -1 * strlen($eol)); + } + return $out; + } } diff --git a/lib/Cake/Network/Email/CakeEmail.php b/lib/Cake/Network/Email/CakeEmail.php index ee4da73d..95207026 100755 --- a/lib/Cake/Network/Email/CakeEmail.php +++ b/lib/Cake/Network/Email/CakeEmail.php @@ -28,1735 +28,1762 @@ * * @package Cake.Network.Email */ -class CakeEmail { - -/** - * Default X-Mailer - * - * @var string - */ - const EMAIL_CLIENT = 'CakePHP Email'; - -/** - * Line length - no should more - RFC 2822 - 2.1.1 - * - * @var int - */ - const LINE_LENGTH_SHOULD = 78; - -/** - * Line length - no must more - RFC 2822 - 2.1.1 - * - * @var int - */ - const LINE_LENGTH_MUST = 998; - -/** - * Type of message - HTML - * - * @var string - */ - const MESSAGE_HTML = 'html'; - -/** - * Type of message - TEXT - * - * @var string - */ - const MESSAGE_TEXT = 'text'; - -/** - * Holds the regex pattern for email validation - * - * @var string - */ - const EMAIL_PATTERN = '/^((?:[\p{L}0-9.!#$%&\'*+\/=?^_`{|}~-]+)*@[\p{L}0-9-_.]+)$/ui'; - -/** - * Recipient of the email - * - * @var array - */ - protected $_to = array(); - -/** - * The mail which the email is sent from - * - * @var array - */ - protected $_from = array(); - -/** - * The sender email - * - * @var array - */ - protected $_sender = array(); - -/** - * The email the recipient will reply to - * - * @var array - */ - protected $_replyTo = array(); - -/** - * The read receipt email - * - * @var array - */ - protected $_readReceipt = array(); - -/** - * The mail that will be used in case of any errors like - * - Remote mailserver down - * - Remote user has exceeded his quota - * - Unknown user - * - * @var array - */ - protected $_returnPath = array(); - -/** - * Carbon Copy - * - * List of email's that should receive a copy of the email. - * The Recipient WILL be able to see this list - * - * @var array - */ - protected $_cc = array(); - -/** - * Blind Carbon Copy - * - * List of email's that should receive a copy of the email. - * The Recipient WILL NOT be able to see this list - * - * @var array - */ - protected $_bcc = array(); - -/** - * Message ID - * - * @var bool|string - */ - protected $_messageId = true; - -/** - * Domain for messageId generation. - * Needs to be manually set for CLI mailing as env('HTTP_HOST') is empty - * - * @var string - */ - protected $_domain = null; - -/** - * The subject of the email - * - * @var string - */ - protected $_subject = ''; - -/** - * Associative array of a user defined headers - * Keys will be prefixed 'X-' as per RFC2822 Section 4.7.5 - * - * @var array - */ - protected $_headers = array(); - -/** - * Layout for the View - * - * @var string - */ - protected $_layout = 'default'; - -/** - * Template for the view - * - * @var string - */ - protected $_template = ''; - -/** - * View for render - * - * @var string - */ - protected $_viewRender = 'View'; - -/** - * Vars to sent to render - * - * @var array - */ - protected $_viewVars = array(); - -/** - * Theme for the View - * - * @var array - */ - protected $_theme = null; - -/** - * Helpers to be used in the render - * - * @var array - */ - protected $_helpers = array('Html'); - -/** - * Text message - * - * @var string - */ - protected $_textMessage = ''; - -/** - * Html message - * - * @var string - */ - protected $_htmlMessage = ''; - -/** - * Final message to send - * - * @var array - */ - protected $_message = array(); - -/** - * Available formats to be sent. - * - * @var array - */ - protected $_emailFormatAvailable = array('text', 'html', 'both'); - -/** - * What format should the email be sent in - * - * @var string - */ - protected $_emailFormat = 'text'; - -/** - * What method should the email be sent - * - * @var string - */ - protected $_transportName = 'Mail'; - -/** - * Instance of transport class - * - * @var AbstractTransport - */ - protected $_transportClass = null; - -/** - * Charset the email body is sent in - * - * @var string - */ - public $charset = 'utf-8'; - -/** - * Charset the email header is sent in - * If null, the $charset property will be used as default - * - * @var string - */ - public $headerCharset = null; - -/** - * The application wide charset, used to encode headers and body - * - * @var string - */ - protected $_appCharset = null; - -/** - * List of files that should be attached to the email. - * - * Only absolute paths - * - * @var array - */ - protected $_attachments = array(); - -/** - * If set, boundary to use for multipart mime messages - * - * @var string - */ - protected $_boundary = null; - -/** - * Configuration to transport - * - * @var string|array - */ - protected $_config = array(); - -/** - * 8Bit character sets - * - * @var array - */ - protected $_charset8bit = array('UTF-8', 'SHIFT_JIS'); - -/** - * Define Content-Type charset name - * - * @var array - */ - protected $_contentTypeCharset = array( - 'ISO-2022-JP-MS' => 'ISO-2022-JP' - ); - -/** - * Regex for email validation - * - * If null, filter_var() will be used. Use the emailPattern() method - * to set a custom pattern.' - * - * @var string - */ - protected $_emailPattern = self::EMAIL_PATTERN; - -/** - * The class name used for email configuration. - * - * @var string - */ - protected $_configClass = 'EmailConfig'; - -/** - * An instance of the EmailConfig class can be set here - * - * @var EmailConfig - */ - protected $_configInstance; - -/** - * Constructor - * - * @param array|string $config Array of configs, or string to load configs from email.php - */ - public function __construct($config = null) { - $this->_appCharset = Configure::read('App.encoding'); - if ($this->_appCharset !== null) { - $this->charset = $this->_appCharset; - } - $this->_domain = preg_replace('/\:\d+$/', '', env('HTTP_HOST')); - if (empty($this->_domain)) { - $this->_domain = php_uname('n'); - } - - if ($config) { - $this->config($config); - } elseif (config('email') && class_exists($this->_configClass)) { - $this->_configInstance = new $this->_configClass(); - if (isset($this->_configInstance->default)) { - $this->config('default'); - } - } - if (empty($this->headerCharset)) { - $this->headerCharset = $this->charset; - } - } - -/** - * From - * - * @param string|array $email Null to get, String with email, - * Array with email as key, name as value or email as value (without name) - * @param string $name Name - * @return array|CakeEmail - * @throws SocketException - */ - public function from($email = null, $name = null) { - if ($email === null) { - return $this->_from; - } - return $this->_setEmailSingle('_from', $email, $name, __d('cake_dev', 'From requires only 1 email address.')); - } - -/** - * Sender - * - * @param string|array $email Null to get, String with email, - * Array with email as key, name as value or email as value (without name) - * @param string $name Name - * @return array|CakeEmail - * @throws SocketException - */ - public function sender($email = null, $name = null) { - if ($email === null) { - return $this->_sender; - } - return $this->_setEmailSingle('_sender', $email, $name, __d('cake_dev', 'Sender requires only 1 email address.')); - } - -/** - * Reply-To - * - * @param string|array $email Null to get, String with email, - * Array with email as key, name as value or email as value (without name) - * @param string $name Name - * @return array|CakeEmail - * @throws SocketException - */ - public function replyTo($email = null, $name = null) { - if ($email === null) { - return $this->_replyTo; - } - return $this->_setEmailSingle('_replyTo', $email, $name, __d('cake_dev', 'Reply-To requires only 1 email address.')); - } - -/** - * Read Receipt (Disposition-Notification-To header) - * - * @param string|array $email Null to get, String with email, - * Array with email as key, name as value or email as value (without name) - * @param string $name Name - * @return array|CakeEmail - * @throws SocketException - */ - public function readReceipt($email = null, $name = null) { - if ($email === null) { - return $this->_readReceipt; - } - return $this->_setEmailSingle('_readReceipt', $email, $name, __d('cake_dev', 'Disposition-Notification-To requires only 1 email address.')); - } - -/** - * Return Path - * - * @param string|array $email Null to get, String with email, - * Array with email as key, name as value or email as value (without name) - * @param string $name Name - * @return array|CakeEmail - * @throws SocketException - */ - public function returnPath($email = null, $name = null) { - if ($email === null) { - return $this->_returnPath; - } - return $this->_setEmailSingle('_returnPath', $email, $name, __d('cake_dev', 'Return-Path requires only 1 email address.')); - } - -/** - * To - * - * @param string|array $email Null to get, String with email, - * Array with email as key, name as value or email as value (without name) - * @param string $name Name - * @return array|self - */ - public function to($email = null, $name = null) { - if ($email === null) { - return $this->_to; - } - return $this->_setEmail('_to', $email, $name); - } - -/** - * Add To - * - * @param string|array $email Null to get, String with email, - * Array with email as key, name as value or email as value (without name) - * @param string $name Name - * @return self - */ - public function addTo($email, $name = null) { - return $this->_addEmail('_to', $email, $name); - } - -/** - * Cc - * - * @param string|array $email Null to get, String with email, - * Array with email as key, name as value or email as value (without name) - * @param string $name Name - * @return array|self - */ - public function cc($email = null, $name = null) { - if ($email === null) { - return $this->_cc; - } - return $this->_setEmail('_cc', $email, $name); - } - -/** - * Add Cc - * - * @param string|array $email Null to get, String with email, - * Array with email as key, name as value or email as value (without name) - * @param string $name Name - * @return self - */ - public function addCc($email, $name = null) { - return $this->_addEmail('_cc', $email, $name); - } - -/** - * Bcc - * - * @param string|array $email Null to get, String with email, - * Array with email as key, name as value or email as value (without name) - * @param string $name Name - * @return array|self - */ - public function bcc($email = null, $name = null) { - if ($email === null) { - return $this->_bcc; - } - return $this->_setEmail('_bcc', $email, $name); - } - -/** - * Add Bcc - * - * @param string|array $email Null to get, String with email, - * Array with email as key, name as value or email as value (without name) - * @param string $name Name - * @return self - */ - public function addBcc($email, $name = null) { - return $this->_addEmail('_bcc', $email, $name); - } - -/** - * Charset setter/getter - * - * @param string $charset Character set. - * @return string this->charset - */ - public function charset($charset = null) { - if ($charset === null) { - return $this->charset; - } - $this->charset = $charset; - if (empty($this->headerCharset)) { - $this->headerCharset = $charset; - } - return $this->charset; - } - -/** - * HeaderCharset setter/getter - * - * @param string $charset Character set. - * @return string this->charset - */ - public function headerCharset($charset = null) { - if ($charset === null) { - return $this->headerCharset; - } - return $this->headerCharset = $charset; - } - -/** - * EmailPattern setter/getter - * - * @param string|bool|null $regex The pattern to use for email address validation, - * null to unset the pattern and make use of filter_var() instead, false or - * nothing to return the current value - * @return string|self - */ - public function emailPattern($regex = false) { - if ($regex === false) { - return $this->_emailPattern; - } - $this->_emailPattern = $regex; - return $this; - } - -/** - * Set email - * - * @param string $varName Property name - * @param string|array $email String with email, - * Array with email as key, name as value or email as value (without name) - * @param string $name Name - * @return self - */ - protected function _setEmail($varName, $email, $name) { - if (!is_array($email)) { - $this->_validateEmail($email, $varName); - if ($name === null) { - $name = $email; - } - $this->{$varName} = array($email => $name); - return $this; - } - $list = array(); - foreach ($email as $key => $value) { - if (is_int($key)) { - $key = $value; - } - $this->_validateEmail($key, $varName); - $list[$key] = $value; - } - $this->{$varName} = $list; - return $this; - } - -/** - * Validate email address - * - * @param string $email Email address to validate - * @param string $context Which property was set - * @return void - * @throws SocketException If email address does not validate - */ - protected function _validateEmail($email, $context) { - if ($this->_emailPattern === null) { - if (filter_var($email, FILTER_VALIDATE_EMAIL)) { - return; - } - } elseif (preg_match($this->_emailPattern, $email)) { - return; - } - if ($email == '') { - throw new SocketException(__d('cake_dev', 'The email set for "%s" is empty.', $context)); - } - throw new SocketException(__d('cake_dev', 'Invalid email set for "%s". You passed "%s".', $context, $email)); - } - -/** - * Set only 1 email - * - * @param string $varName Property name - * @param string|array $email String with email, - * Array with email as key, name as value or email as value (without name) - * @param string $name Name - * @param string $throwMessage Exception message - * @return self - * @throws SocketException - */ - protected function _setEmailSingle($varName, $email, $name, $throwMessage) { - $current = $this->{$varName}; - $this->_setEmail($varName, $email, $name); - if (count($this->{$varName}) !== 1) { - $this->{$varName} = $current; - throw new SocketException($throwMessage); - } - return $this; - } - -/** - * Add email - * - * @param string $varName Property name - * @param string|array $email String with email, - * Array with email as key, name as value or email as value (without name) - * @param string $name Name - * @return self - * @throws SocketException - */ - protected function _addEmail($varName, $email, $name) { - if (!is_array($email)) { - $this->_validateEmail($email, $varName); - if ($name === null) { - $name = $email; - } - $this->{$varName}[$email] = $name; - return $this; - } - $list = array(); - foreach ($email as $key => $value) { - if (is_int($key)) { - $key = $value; - } - $this->_validateEmail($key, $varName); - $list[$key] = $value; - } - $this->{$varName} = array_merge($this->{$varName}, $list); - return $this; - } - -/** - * Get/Set Subject. - * - * @param string $subject Subject string. - * @return string|self - */ - public function subject($subject = null) { - if ($subject === null) { - return $this->_subject; - } - $this->_subject = $this->_encode((string)$subject); - return $this; - } - -/** - * Sets headers for the message - * - * @param array $headers Associative array containing headers to be set. - * @return self - * @throws SocketException - */ - public function setHeaders($headers) { - if (!is_array($headers)) { - throw new SocketException(__d('cake_dev', '$headers should be an array.')); - } - $this->_headers = $headers; - return $this; - } - -/** - * Add header for the message - * - * @param array $headers Headers to set. - * @return self - * @throws SocketException - */ - public function addHeaders($headers) { - if (!is_array($headers)) { - throw new SocketException(__d('cake_dev', '$headers should be an array.')); - } - $this->_headers = array_merge($this->_headers, $headers); - return $this; - } - -/** - * Get list of headers - * - * ### Includes: - * - * - `from` - * - `replyTo` - * - `readReceipt` - * - `returnPath` - * - `to` - * - `cc` - * - `bcc` - * - `subject` - * - * @param array $include List of headers. - * @return array - */ - public function getHeaders($include = array()) { - if ($include == array_values($include)) { - $include = array_fill_keys($include, true); - } - $defaults = array_fill_keys( - array( - 'from', 'sender', 'replyTo', 'readReceipt', 'returnPath', - 'to', 'cc', 'bcc', 'subject'), - false - ); - $include += $defaults; - - $headers = array(); - $relation = array( - 'from' => 'From', - 'replyTo' => 'Reply-To', - 'readReceipt' => 'Disposition-Notification-To', - 'returnPath' => 'Return-Path' - ); - foreach ($relation as $var => $header) { - if ($include[$var]) { - $var = '_' . $var; - $headers[$header] = current($this->_formatAddress($this->{$var})); - } - } - if ($include['sender']) { - if (key($this->_sender) === key($this->_from)) { - $headers['Sender'] = ''; - } else { - $headers['Sender'] = current($this->_formatAddress($this->_sender)); - } - } - - foreach (array('to', 'cc', 'bcc') as $var) { - if ($include[$var]) { - $classVar = '_' . $var; - $headers[ucfirst($var)] = implode(', ', $this->_formatAddress($this->{$classVar})); - } - } - - $headers += $this->_headers; - if (!isset($headers['X-Mailer'])) { - $headers['X-Mailer'] = static::EMAIL_CLIENT; - } - if (!isset($headers['Date'])) { - $headers['Date'] = date(DATE_RFC2822); - } - if ($this->_messageId !== false) { - if ($this->_messageId === true) { - $headers['Message-ID'] = '<' . str_replace('-', '', CakeText::uuid()) . '@' . $this->_domain . '>'; - } else { - $headers['Message-ID'] = $this->_messageId; - } - } - - if ($include['subject']) { - $headers['Subject'] = $this->_subject; - } - - $headers['MIME-Version'] = '1.0'; - if (!empty($this->_attachments)) { - $headers['Content-Type'] = 'multipart/mixed; boundary="' . $this->_boundary . '"'; - } elseif ($this->_emailFormat === 'both') { - $headers['Content-Type'] = 'multipart/alternative; boundary="' . $this->_boundary . '"'; - } elseif ($this->_emailFormat === 'text') { - $headers['Content-Type'] = 'text/plain; charset=' . $this->_getContentTypeCharset(); - } elseif ($this->_emailFormat === 'html') { - $headers['Content-Type'] = 'text/html; charset=' . $this->_getContentTypeCharset(); - } - $headers['Content-Transfer-Encoding'] = $this->_getContentTransferEncoding(); - - return $headers; - } - -/** - * Format addresses - * - * If the address contains non alphanumeric/whitespace characters, it will - * be quoted as characters like `:` and `,` are known to cause issues - * in address header fields. - * - * @param array $address Addresses to format. - * @return array - */ - protected function _formatAddress($address) { - $return = array(); - foreach ($address as $email => $alias) { - if ($email === $alias) { - $return[] = $email; - } else { - $encoded = $this->_encode($alias); - if ( - $encoded === $alias && preg_match('/[^a-z0-9 ]/i', $encoded) || - strpos($encoded, ',') !== false - ) { - $encoded = '"' . str_replace('"', '\"', $encoded) . '"'; - } - $return[] = sprintf('%s <%s>', $encoded, $email); - } - } - return $return; - } - -/** - * Template and layout - * - * @param bool|string $template Template name or null to not use - * @param bool|string $layout Layout name or null to not use - * @return array|self - */ - public function template($template = false, $layout = false) { - if ($template === false) { - return array( - 'template' => $this->_template, - 'layout' => $this->_layout - ); - } - $this->_template = $template; - if ($layout !== false) { - $this->_layout = $layout; - } - return $this; - } - -/** - * View class for render - * - * @param string $viewClass View class name. - * @return string|self - */ - public function viewRender($viewClass = null) { - if ($viewClass === null) { - return $this->_viewRender; - } - $this->_viewRender = $viewClass; - return $this; - } - -/** - * Variables to be set on render - * - * @param array $viewVars Variables to set for view. - * @return array|self - */ - public function viewVars($viewVars = null) { - if ($viewVars === null) { - return $this->_viewVars; - } - $this->_viewVars = array_merge($this->_viewVars, (array)$viewVars); - return $this; - } - -/** - * Theme to use when rendering - * - * @param string $theme Theme name. - * @return string|self - */ - public function theme($theme = null) { - if ($theme === null) { - return $this->_theme; - } - $this->_theme = $theme; - return $this; - } - -/** - * Helpers to be used in render - * - * @param array $helpers Helpers list. - * @return array|self - */ - public function helpers($helpers = null) { - if ($helpers === null) { - return $this->_helpers; - } - $this->_helpers = (array)$helpers; - return $this; - } - -/** - * Email format - * - * @param string $format Formatting string. - * @return string|self - * @throws SocketException - */ - public function emailFormat($format = null) { - if ($format === null) { - return $this->_emailFormat; - } - if (!in_array($format, $this->_emailFormatAvailable)) { - throw new SocketException(__d('cake_dev', 'Format not available.')); - } - $this->_emailFormat = $format; - return $this; - } - -/** - * Transport name - * - * @param string $name Transport name. - * @return string|self - */ - public function transport($name = null) { - if ($name === null) { - return $this->_transportName; - } - $this->_transportName = (string)$name; - $this->_transportClass = null; - return $this; - } - -/** - * Return the transport class - * - * @return AbstractTransport - * @throws SocketException - */ - public function transportClass() { - if ($this->_transportClass) { - return $this->_transportClass; - } - list($plugin, $transportClassname) = pluginSplit($this->_transportName, true); - $transportClassname .= 'Transport'; - App::uses($transportClassname, $plugin . 'Network/Email'); - if (!class_exists($transportClassname)) { - throw new SocketException(__d('cake_dev', 'Class "%s" not found.', $transportClassname)); - } elseif (!method_exists($transportClassname, 'send')) { - throw new SocketException(__d('cake_dev', 'The "%s" does not have a %s method.', $transportClassname, 'send()')); - } - - return $this->_transportClass = new $transportClassname(); - } - -/** - * Message-ID - * - * @param bool|string $message True to generate a new Message-ID, False to ignore (not send in email), String to set as Message-ID - * @return bool|string|self - * @throws SocketException - */ - public function messageId($message = null) { - if ($message === null) { - return $this->_messageId; - } - if (is_bool($message)) { - $this->_messageId = $message; - } else { - if (!preg_match('/^\<.+@.+\>$/', $message)) { - throw new SocketException(__d('cake_dev', 'Invalid format for Message-ID. The text should be something like ""')); - } - $this->_messageId = $message; - } - return $this; - } - -/** - * Domain as top level (the part after @) - * - * @param string $domain Manually set the domain for CLI mailing - * @return string|self - */ - public function domain($domain = null) { - if ($domain === null) { - return $this->_domain; - } - $this->_domain = $domain; - return $this; - } - -/** - * Add attachments to the email message - * - * Attachments can be defined in a few forms depending on how much control you need: - * - * Attach a single file: - * - * ``` - * $email->attachments('path/to/file'); - * ``` - * - * Attach a file with a different filename: - * - * ``` - * $email->attachments(array('custom_name.txt' => 'path/to/file.txt')); - * ``` - * - * Attach a file and specify additional properties: - * - * ``` - * $email->attachments(array('custom_name.png' => array( - * 'file' => 'path/to/file', - * 'mimetype' => 'image/png', - * 'contentId' => 'abc123', - * 'contentDisposition' => false - * )); - * ``` - * - * Attach a file from string and specify additional properties: - * - * ``` - * $email->attachments(array('custom_name.png' => array( - * 'data' => file_get_contents('path/to/file'), - * 'mimetype' => 'image/png' - * )); - * ``` - * - * The `contentId` key allows you to specify an inline attachment. In your email text, you - * can use `` to display the image inline. - * - * The `contentDisposition` key allows you to disable the `Content-Disposition` header, this can improve - * attachment compatibility with outlook email clients. - * - * @param string|array $attachments String with the filename or array with filenames - * @return array|self Either the array of attachments when getting or $this when setting. - * @throws SocketException - */ - public function attachments($attachments = null) { - if ($attachments === null) { - return $this->_attachments; - } - $attach = array(); - foreach ((array)$attachments as $name => $fileInfo) { - if (!is_array($fileInfo)) { - $fileInfo = array('file' => $fileInfo); - } - if (!isset($fileInfo['file'])) { - if (!isset($fileInfo['data'])) { - throw new SocketException(__d('cake_dev', 'No file or data specified.')); - } - if (is_int($name)) { - throw new SocketException(__d('cake_dev', 'No filename specified.')); - } - $fileInfo['data'] = chunk_split(base64_encode($fileInfo['data']), 76, "\r\n"); - } else { - $fileName = $fileInfo['file']; - $fileInfo['file'] = realpath($fileInfo['file']); - if ($fileInfo['file'] === false || !file_exists($fileInfo['file'])) { - throw new SocketException(__d('cake_dev', 'File not found: "%s"', $fileName)); - } - if (is_int($name)) { - $name = basename($fileInfo['file']); - } - } - if (!isset($fileInfo['mimetype']) && isset($fileInfo['file']) && function_exists('mime_content_type')) { - $fileInfo['mimetype'] = mime_content_type($fileInfo['file']); - } - if (!isset($fileInfo['mimetype'])) { - $fileInfo['mimetype'] = 'application/octet-stream'; - } - $attach[$name] = $fileInfo; - } - $this->_attachments = $attach; - return $this; - } - -/** - * Add attachments - * - * @param string|array $attachments String with the filename or array with filenames - * @return self - * @throws SocketException - * @see CakeEmail::attachments() - */ - public function addAttachments($attachments) { - $current = $this->_attachments; - $this->attachments($attachments); - $this->_attachments = array_merge($current, $this->_attachments); - return $this; - } - -/** - * Get generated message (used by transport classes) - * - * @param string $type Use MESSAGE_* constants or null to return the full message as array - * @return string|array String if have type, array if type is null - */ - public function message($type = null) { - switch ($type) { - case static::MESSAGE_HTML: - return $this->_htmlMessage; - case static::MESSAGE_TEXT: - return $this->_textMessage; - } - return $this->_message; - } - -/** - * Configuration to use when send email - * - * ### Usage - * - * Load configuration from `app/Config/email.php`: - * - * `$email->config('default');` - * - * Merge an array of configuration into the instance: - * - * `$email->config(array('to' => 'bill@example.com'));` - * - * @param string|array $config String with configuration name (from email.php), array with config or null to return current config - * @return string|array|self - */ - public function config($config = null) { - if ($config === null) { - return $this->_config; - } - if (!is_array($config)) { - $config = (string)$config; - } - - $this->_applyConfig($config); - return $this; - } - -/** - * Send an email using the specified content, template and layout - * - * @param string|array $content String with message or array with messages - * @return array - * @throws SocketException - */ - public function send($content = null) { - if (empty($this->_from)) { - throw new SocketException(__d('cake_dev', 'From is not specified.')); - } - if (empty($this->_to) && empty($this->_cc) && empty($this->_bcc)) { - throw new SocketException(__d('cake_dev', 'You need to specify at least one destination for to, cc or bcc.')); - } - - if (is_array($content)) { - $content = implode("\n", $content) . "\n"; - } - - $this->_message = $this->_render($this->_wrap($content)); - - $contents = $this->transportClass()->send($this); - if (!empty($this->_config['log'])) { - $config = array( - 'level' => LOG_DEBUG, - 'scope' => 'email' - ); - if ($this->_config['log'] !== true) { - if (!is_array($this->_config['log'])) { - $this->_config['log'] = array('level' => $this->_config['log']); - } - $config = $this->_config['log'] + $config; - } - CakeLog::write( - $config['level'], - PHP_EOL . $contents['headers'] . PHP_EOL . PHP_EOL . $contents['message'], - $config['scope'] - ); - } - return $contents; - } - -/** - * Static method to fast create an instance of CakeEmail - * - * @param string|array $to Address to send (see CakeEmail::to()). If null, will try to use 'to' from transport config - * @param string $subject String of subject or null to use 'subject' from transport config - * @param string|array $message String with message or array with variables to be used in render - * @param string|array $transportConfig String to use config from EmailConfig or array with configs - * @param bool $send Send the email or just return the instance pre-configured - * @return self Instance of CakeEmail - * @throws SocketException - */ - public static function deliver($to = null, $subject = null, $message = null, $transportConfig = 'fast', $send = true) { - $class = get_called_class(); - /** @var CakeEmail $instance */ - $instance = new $class($transportConfig); - if ($to !== null) { - $instance->to($to); - } - if ($subject !== null) { - $instance->subject($subject); - } - if (is_array($message)) { - $instance->viewVars($message); - $message = null; - } elseif ($message === null && array_key_exists('message', $config = $instance->config())) { - $message = $config['message']; - } - - if ($send === true) { - $instance->send($message); - } - - return $instance; - } - -/** - * Apply the config to an instance - * - * @param array $config Configuration options. - * @return void - * @throws ConfigureException When configuration file cannot be found, or is missing - * the named config. - */ - protected function _applyConfig($config) { - if (is_string($config)) { - if (!$this->_configInstance) { - if (!class_exists($this->_configClass) && !config('email')) { - throw new ConfigureException(__d('cake_dev', '%s not found.', CONFIG . 'email.php')); - } - $this->_configInstance = new $this->_configClass(); - } - if (!isset($this->_configInstance->{$config})) { - throw new ConfigureException(__d('cake_dev', 'Unknown email configuration "%s".', $config)); - } - $config = $this->_configInstance->{$config}; - } - $this->_config = $config + $this->_config; - if (!empty($config['charset'])) { - $this->charset = $config['charset']; - } - if (!empty($config['headerCharset'])) { - $this->headerCharset = $config['headerCharset']; - } - if (empty($this->headerCharset)) { - $this->headerCharset = $this->charset; - } - $simpleMethods = array( - 'from', 'sender', 'to', 'replyTo', 'readReceipt', 'returnPath', 'cc', 'bcc', - 'messageId', 'domain', 'subject', 'viewRender', 'viewVars', 'attachments', - 'transport', 'emailFormat', 'theme', 'helpers', 'emailPattern' - ); - foreach ($simpleMethods as $method) { - if (isset($config[$method])) { - $this->$method($config[$method]); - unset($config[$method]); - } - } - if (isset($config['headers'])) { - $this->setHeaders($config['headers']); - unset($config['headers']); - } - - if (array_key_exists('template', $config)) { - $this->_template = $config['template']; - } - if (array_key_exists('layout', $config)) { - $this->_layout = $config['layout']; - } - - $this->transportClass()->config($config); - } - -/** - * Reset all CakeEmail internal variables to be able to send out a new email. - * - * @return self - */ - public function reset() { - $this->_to = array(); - $this->_from = array(); - $this->_sender = array(); - $this->_replyTo = array(); - $this->_readReceipt = array(); - $this->_returnPath = array(); - $this->_cc = array(); - $this->_bcc = array(); - $this->_messageId = true; - $this->_subject = ''; - $this->_headers = array(); - $this->_layout = 'default'; - $this->_template = ''; - $this->_viewRender = 'View'; - $this->_viewVars = array(); - $this->_theme = null; - $this->_helpers = array('Html'); - $this->_textMessage = ''; - $this->_htmlMessage = ''; - $this->_message = ''; - $this->_emailFormat = 'text'; - $this->_transportName = 'Mail'; - $this->_transportClass = null; - $this->charset = 'utf-8'; - $this->headerCharset = null; - $this->_attachments = array(); - $this->_config = array(); - $this->_emailPattern = static::EMAIL_PATTERN; - return $this; - } - -/** - * Encode the specified string using the current charset - * - * @param string $text String to encode - * @return string Encoded string - */ - protected function _encode($text) { - $internalEncoding = function_exists('mb_internal_encoding'); - if ($internalEncoding) { - $restore = mb_internal_encoding(); - mb_internal_encoding($this->_appCharset); - } - if (empty($this->headerCharset)) { - $this->headerCharset = $this->charset; - } - $return = mb_encode_mimeheader($text, $this->headerCharset, 'B'); - if ($internalEncoding) { - mb_internal_encoding($restore); - } - return $return; - } - -/** - * Translates a string for one charset to another if the App.encoding value - * differs and the mb_convert_encoding function exists - * - * @param string $text The text to be converted - * @param string $charset the target encoding - * @return string - */ - protected function _encodeString($text, $charset) { - if ($this->_appCharset === $charset || !function_exists('mb_convert_encoding')) { - return $text; - } - return mb_convert_encoding($text, $charset, $this->_appCharset); - } - -/** - * Wrap the message to follow the RFC 2822 - 2.1.1 - * - * @param string $message Message to wrap - * @param int $wrapLength The line length - * @return array Wrapped message - */ - protected function _wrap($message, $wrapLength = CakeEmail::LINE_LENGTH_MUST) { - if (strlen($message) === 0) { - return array(''); - } - $message = str_replace(array("\r\n", "\r"), "\n", $message); - $lines = explode("\n", $message); - $formatted = array(); - $cut = ($wrapLength == CakeEmail::LINE_LENGTH_MUST); - - foreach ($lines as $line) { - if (empty($line) && $line !== '0') { - $formatted[] = ''; - continue; - } - if (strlen($line) < $wrapLength) { - $formatted[] = $line; - continue; - } - if (!preg_match('/<[a-z]+.*>/i', $line)) { - $formatted = array_merge( - $formatted, - explode("\n", wordwrap($line, $wrapLength, "\n", $cut)) - ); - continue; - } - - $tagOpen = false; - $tmpLine = $tag = ''; - $tmpLineLength = 0; - for ($i = 0, $count = strlen($line); $i < $count; $i++) { - $char = $line[$i]; - if ($tagOpen) { - $tag .= $char; - if ($char === '>') { - $tagLength = strlen($tag); - if ($tagLength + $tmpLineLength < $wrapLength) { - $tmpLine .= $tag; - $tmpLineLength += $tagLength; - } else { - if ($tmpLineLength > 0) { - $formatted = array_merge( - $formatted, - explode("\n", wordwrap(trim($tmpLine), $wrapLength, "\n", $cut)) - ); - $tmpLine = ''; - $tmpLineLength = 0; - } - if ($tagLength > $wrapLength) { - $formatted[] = $tag; - } else { - $tmpLine = $tag; - $tmpLineLength = $tagLength; - } - } - $tag = ''; - $tagOpen = false; - } - continue; - } - if ($char === '<') { - $tagOpen = true; - $tag = '<'; - continue; - } - if ($char === ' ' && $tmpLineLength >= $wrapLength) { - $formatted[] = $tmpLine; - $tmpLineLength = 0; - continue; - } - $tmpLine .= $char; - $tmpLineLength++; - if ($tmpLineLength === $wrapLength) { - $nextChar = isset($line[$i + 1]) ? $line[$i + 1] : ''; - if ($nextChar === ' ' || $nextChar === '<') { - $formatted[] = trim($tmpLine); - $tmpLine = ''; - $tmpLineLength = 0; - if ($nextChar === ' ') { - $i++; - } - } else { - $lastSpace = strrpos($tmpLine, ' '); - if ($lastSpace === false) { - continue; - } - $formatted[] = trim(substr($tmpLine, 0, $lastSpace)); - $tmpLine = substr($tmpLine, $lastSpace + 1); - - $tmpLineLength = strlen($tmpLine); - } - } - } - if (!empty($tmpLine)) { - $formatted[] = $tmpLine; - } - } - $formatted[] = ''; - return $formatted; - } - -/** - * Create unique boundary identifier - * - * @return void - */ - protected function _createBoundary() { - if (!empty($this->_attachments) || $this->_emailFormat === 'both') { - $this->_boundary = md5(uniqid(time())); - } - } - -/** - * Attach non-embedded files by adding file contents inside boundaries. - * - * @param string $boundary Boundary to use. If null, will default to $this->_boundary - * @return array An array of lines to add to the message - */ - protected function _attachFiles($boundary = null) { - if ($boundary === null) { - $boundary = $this->_boundary; - } - - $msg = array(); - foreach ($this->_attachments as $filename => $fileInfo) { - if (!empty($fileInfo['contentId'])) { - continue; - } - $data = isset($fileInfo['data']) ? $fileInfo['data'] : $this->_readFile($fileInfo['file']); - - $msg[] = '--' . $boundary; - $msg[] = 'Content-Type: ' . $fileInfo['mimetype']; - $msg[] = 'Content-Transfer-Encoding: base64'; - if (!isset($fileInfo['contentDisposition']) || - $fileInfo['contentDisposition'] - ) { - $msg[] = 'Content-Disposition: attachment; filename="' . $filename . '"'; - } - $msg[] = ''; - $msg[] = $data; - $msg[] = ''; - } - return $msg; - } - -/** - * Read the file contents and return a base64 version of the file contents. - * - * @param string $path The absolute path to the file to read. - * @return string File contents in base64 encoding - */ - protected function _readFile($path) { - $File = new File($path); - return chunk_split(base64_encode($File->read())); - } - -/** - * Attach inline/embedded files to the message. - * - * @param string $boundary Boundary to use. If null, will default to $this->_boundary - * @return array An array of lines to add to the message - */ - protected function _attachInlineFiles($boundary = null) { - if ($boundary === null) { - $boundary = $this->_boundary; - } - - $msg = array(); - foreach ($this->_attachments as $filename => $fileInfo) { - if (empty($fileInfo['contentId'])) { - continue; - } - $data = isset($fileInfo['data']) ? $fileInfo['data'] : $this->_readFile($fileInfo['file']); - - $msg[] = '--' . $boundary; - $msg[] = 'Content-Type: ' . $fileInfo['mimetype']; - $msg[] = 'Content-Transfer-Encoding: base64'; - $msg[] = 'Content-ID: <' . $fileInfo['contentId'] . '>'; - $msg[] = 'Content-Disposition: inline; filename="' . $filename . '"'; - $msg[] = ''; - $msg[] = $data; - $msg[] = ''; - } - return $msg; - } - -/** - * Render the body of the email. - * - * @param array $content Content to render - * @return array Email body ready to be sent - */ - protected function _render($content) { - $this->_textMessage = $this->_htmlMessage = ''; - - $content = implode("\n", $content); - $rendered = $this->_renderTemplates($content); - - $this->_createBoundary(); - $msg = array(); - - $contentIds = array_filter((array)Hash::extract($this->_attachments, '{s}.contentId')); - $hasInlineAttachments = count($contentIds) > 0; - $hasAttachments = !empty($this->_attachments); - $hasMultipleTypes = count($rendered) > 1; - $multiPart = ($hasAttachments || $hasMultipleTypes); - - $boundary = $relBoundary = $textBoundary = $this->_boundary; - - if ($hasInlineAttachments) { - $msg[] = '--' . $boundary; - $msg[] = 'Content-Type: multipart/related; boundary="rel-' . $boundary . '"'; - $msg[] = ''; - $relBoundary = $textBoundary = 'rel-' . $boundary; - } - - if ($hasMultipleTypes && $hasAttachments) { - $msg[] = '--' . $relBoundary; - $msg[] = 'Content-Type: multipart/alternative; boundary="alt-' . $boundary . '"'; - $msg[] = ''; - $textBoundary = 'alt-' . $boundary; - } - - if (isset($rendered['text'])) { - if ($multiPart) { - $msg[] = '--' . $textBoundary; - $msg[] = 'Content-Type: text/plain; charset=' . $this->_getContentTypeCharset(); - $msg[] = 'Content-Transfer-Encoding: ' . $this->_getContentTransferEncoding(); - $msg[] = ''; - } - $this->_textMessage = $rendered['text']; - $content = explode("\n", $this->_textMessage); - $msg = array_merge($msg, $content); - $msg[] = ''; - } - - if (isset($rendered['html'])) { - if ($multiPart) { - $msg[] = '--' . $textBoundary; - $msg[] = 'Content-Type: text/html; charset=' . $this->_getContentTypeCharset(); - $msg[] = 'Content-Transfer-Encoding: ' . $this->_getContentTransferEncoding(); - $msg[] = ''; - } - $this->_htmlMessage = $rendered['html']; - $content = explode("\n", $this->_htmlMessage); - $msg = array_merge($msg, $content); - $msg[] = ''; - } - - if ($textBoundary !== $relBoundary) { - $msg[] = '--' . $textBoundary . '--'; - $msg[] = ''; - } - - if ($hasInlineAttachments) { - $attachments = $this->_attachInlineFiles($relBoundary); - $msg = array_merge($msg, $attachments); - $msg[] = ''; - $msg[] = '--' . $relBoundary . '--'; - $msg[] = ''; - } - - if ($hasAttachments) { - $attachments = $this->_attachFiles($boundary); - $msg = array_merge($msg, $attachments); - } - if ($hasAttachments || $hasMultipleTypes) { - $msg[] = ''; - $msg[] = '--' . $boundary . '--'; - $msg[] = ''; - } - return $msg; - } - -/** - * Gets the text body types that are in this email message - * - * @return array Array of types. Valid types are 'text' and 'html' - */ - protected function _getTypes() { - $types = array($this->_emailFormat); - if ($this->_emailFormat === 'both') { - $types = array('html', 'text'); - } - return $types; - } - -/** - * Build and set all the view properties needed to render the templated emails. - * If there is no template set, the $content will be returned in a hash - * of the text content types for the email. - * - * @param string $content The content passed in from send() in most cases. - * @return array The rendered content with html and text keys. - */ - protected function _renderTemplates($content) { - $types = $this->_getTypes(); - $rendered = array(); - if (empty($this->_template)) { - foreach ($types as $type) { - $rendered[$type] = $this->_encodeString($content, $this->charset); - } - return $rendered; - } - $viewClass = $this->_viewRender; - if ($viewClass !== 'View') { - list($plugin, $viewClass) = pluginSplit($viewClass, true); - $viewClass .= 'View'; - App::uses($viewClass, $plugin . 'View'); - } - - /** @var View $View */ - $View = new $viewClass(null); - $View->viewVars = $this->_viewVars; - $View->helpers = $this->_helpers; - - if ($this->_theme) { - $View->theme = $this->_theme; - } - - $View->loadHelpers(); - - list($templatePlugin, $template) = pluginSplit($this->_template); - list($layoutPlugin, $layout) = pluginSplit($this->_layout); - if ($templatePlugin) { - $View->plugin = $templatePlugin; - } elseif ($layoutPlugin) { - $View->plugin = $layoutPlugin; - } - - if ($View->get('content') === null) { - $View->set('content', $content); - } - - // Convert null to false, as View needs false to disable - // the layout. - if ($this->_layout === null) { - $this->_layout = false; - } - - foreach ($types as $type) { - $View->hasRendered = false; - $View->viewPath = $View->layoutPath = 'Emails' . DS . $type; - - $render = $View->render($this->_template, $this->_layout); - $render = str_replace(array("\r\n", "\r"), "\n", $render); - $rendered[$type] = $this->_encodeString($render, $this->charset); - } - - foreach ($rendered as $type => $content) { - $rendered[$type] = $this->_wrap($content); - $rendered[$type] = implode("\n", $rendered[$type]); - $rendered[$type] = rtrim($rendered[$type], "\n"); - } - return $rendered; - } - -/** - * Return the Content-Transfer Encoding value based on the set charset - * - * @return string - */ - protected function _getContentTransferEncoding() { - $charset = strtoupper($this->charset); - if (in_array($charset, $this->_charset8bit)) { - return '8bit'; - } - return '7bit'; - } - -/** - * Return charset value for Content-Type. - * - * Checks fallback/compatibility types which include workarounds - * for legacy japanese character sets. - * - * @return string - */ - protected function _getContentTypeCharset() { - $charset = strtoupper($this->charset); - if (array_key_exists($charset, $this->_contentTypeCharset)) { - return strtoupper($this->_contentTypeCharset[$charset]); - } - return strtoupper($this->charset); - } +class CakeEmail +{ + + /** + * Default X-Mailer + * + * @var string + */ + const EMAIL_CLIENT = 'CakePHP Email'; + + /** + * Line length - no should more - RFC 2822 - 2.1.1 + * + * @var int + */ + const LINE_LENGTH_SHOULD = 78; + + /** + * Line length - no must more - RFC 2822 - 2.1.1 + * + * @var int + */ + const LINE_LENGTH_MUST = 998; + + /** + * Type of message - HTML + * + * @var string + */ + const MESSAGE_HTML = 'html'; + + /** + * Type of message - TEXT + * + * @var string + */ + const MESSAGE_TEXT = 'text'; + + /** + * Holds the regex pattern for email validation + * + * @var string + */ + const EMAIL_PATTERN = '/^((?:[\p{L}0-9.!#$%&\'*+\/=?^_`{|}~-]+)*@[\p{L}0-9-_.]+)$/ui'; + /** + * Charset the email body is sent in + * + * @var string + */ + public $charset = 'utf-8'; + /** + * Charset the email header is sent in + * If null, the $charset property will be used as default + * + * @var string + */ + public $headerCharset = null; + /** + * Recipient of the email + * + * @var array + */ + protected $_to = []; + /** + * The mail which the email is sent from + * + * @var array + */ + protected $_from = []; + /** + * The sender email + * + * @var array + */ + protected $_sender = []; + /** + * The email the recipient will reply to + * + * @var array + */ + protected $_replyTo = []; + /** + * The read receipt email + * + * @var array + */ + protected $_readReceipt = []; + /** + * The mail that will be used in case of any errors like + * - Remote mailserver down + * - Remote user has exceeded his quota + * - Unknown user + * + * @var array + */ + protected $_returnPath = []; + /** + * Carbon Copy + * + * List of email's that should receive a copy of the email. + * The Recipient WILL be able to see this list + * + * @var array + */ + protected $_cc = []; + /** + * Blind Carbon Copy + * + * List of email's that should receive a copy of the email. + * The Recipient WILL NOT be able to see this list + * + * @var array + */ + protected $_bcc = []; + /** + * Message ID + * + * @var bool|string + */ + protected $_messageId = true; + /** + * Domain for messageId generation. + * Needs to be manually set for CLI mailing as env('HTTP_HOST') is empty + * + * @var string + */ + protected $_domain = null; + /** + * The subject of the email + * + * @var string + */ + protected $_subject = ''; + /** + * Associative array of a user defined headers + * Keys will be prefixed 'X-' as per RFC2822 Section 4.7.5 + * + * @var array + */ + protected $_headers = []; + /** + * Layout for the View + * + * @var string + */ + protected $_layout = 'default'; + /** + * Template for the view + * + * @var string + */ + protected $_template = ''; + /** + * View for render + * + * @var string + */ + protected $_viewRender = 'View'; + /** + * Vars to sent to render + * + * @var array + */ + protected $_viewVars = []; + /** + * Theme for the View + * + * @var array + */ + protected $_theme = null; + /** + * Helpers to be used in the render + * + * @var array + */ + protected $_helpers = ['Html']; + /** + * Text message + * + * @var string + */ + protected $_textMessage = ''; + /** + * Html message + * + * @var string + */ + protected $_htmlMessage = ''; + /** + * Final message to send + * + * @var array + */ + protected $_message = []; + /** + * Available formats to be sent. + * + * @var array + */ + protected $_emailFormatAvailable = ['text', 'html', 'both']; + /** + * What format should the email be sent in + * + * @var string + */ + protected $_emailFormat = 'text'; + /** + * What method should the email be sent + * + * @var string + */ + protected $_transportName = 'Mail'; + /** + * Instance of transport class + * + * @var AbstractTransport + */ + protected $_transportClass = null; + /** + * The application wide charset, used to encode headers and body + * + * @var string + */ + protected $_appCharset = null; + + /** + * List of files that should be attached to the email. + * + * Only absolute paths + * + * @var array + */ + protected $_attachments = []; + + /** + * If set, boundary to use for multipart mime messages + * + * @var string + */ + protected $_boundary = null; + + /** + * Configuration to transport + * + * @var string|array + */ + protected $_config = []; + + /** + * 8Bit character sets + * + * @var array + */ + protected $_charset8bit = ['UTF-8', 'SHIFT_JIS']; + + /** + * Define Content-Type charset name + * + * @var array + */ + protected $_contentTypeCharset = [ + 'ISO-2022-JP-MS' => 'ISO-2022-JP' + ]; + + /** + * Regex for email validation + * + * If null, filter_var() will be used. Use the emailPattern() method + * to set a custom pattern.' + * + * @var string + */ + protected $_emailPattern = self::EMAIL_PATTERN; + + /** + * The class name used for email configuration. + * + * @var string + */ + protected $_configClass = 'EmailConfig'; + + /** + * An instance of the EmailConfig class can be set here + * + * @var EmailConfig + */ + protected $_configInstance; + + /** + * Constructor + * + * @param array|string $config Array of configs, or string to load configs from email.php + */ + public function __construct($config = null) + { + $this->_appCharset = Configure::read('App.encoding'); + if ($this->_appCharset !== null) { + $this->charset = $this->_appCharset; + } + $this->_domain = preg_replace('/\:\d+$/', '', env('HTTP_HOST')); + if (empty($this->_domain)) { + $this->_domain = php_uname('n'); + } + + if ($config) { + $this->config($config); + } else if (config('email') && class_exists($this->_configClass)) { + $this->_configInstance = new $this->_configClass(); + if (isset($this->_configInstance->default)) { + $this->config('default'); + } + } + if (empty($this->headerCharset)) { + $this->headerCharset = $this->charset; + } + } + + /** + * Configuration to use when send email + * + * ### Usage + * + * Load configuration from `app/Config/email.php`: + * + * `$email->config('default');` + * + * Merge an array of configuration into the instance: + * + * `$email->config(array('to' => 'bill@example.com'));` + * + * @param string|array $config String with configuration name (from email.php), array with config or null to return current config + * @return string|array|self + */ + public function config($config = null) + { + if ($config === null) { + return $this->_config; + } + if (!is_array($config)) { + $config = (string)$config; + } + + $this->_applyConfig($config); + return $this; + } + + /** + * Apply the config to an instance + * + * @param array $config Configuration options. + * @return void + * @throws ConfigureException When configuration file cannot be found, or is missing + * the named config. + */ + protected function _applyConfig($config) + { + if (is_string($config)) { + if (!$this->_configInstance) { + if (!class_exists($this->_configClass) && !config('email')) { + throw new ConfigureException(__d('cake_dev', '%s not found.', CONFIG . 'email.php')); + } + $this->_configInstance = new $this->_configClass(); + } + if (!isset($this->_configInstance->{$config})) { + throw new ConfigureException(__d('cake_dev', 'Unknown email configuration "%s".', $config)); + } + $config = $this->_configInstance->{$config}; + } + $this->_config = $config + $this->_config; + if (!empty($config['charset'])) { + $this->charset = $config['charset']; + } + if (!empty($config['headerCharset'])) { + $this->headerCharset = $config['headerCharset']; + } + if (empty($this->headerCharset)) { + $this->headerCharset = $this->charset; + } + $simpleMethods = [ + 'from', 'sender', 'to', 'replyTo', 'readReceipt', 'returnPath', 'cc', 'bcc', + 'messageId', 'domain', 'subject', 'viewRender', 'viewVars', 'attachments', + 'transport', 'emailFormat', 'theme', 'helpers', 'emailPattern' + ]; + foreach ($simpleMethods as $method) { + if (isset($config[$method])) { + $this->$method($config[$method]); + unset($config[$method]); + } + } + if (isset($config['headers'])) { + $this->setHeaders($config['headers']); + unset($config['headers']); + } + + if (array_key_exists('template', $config)) { + $this->_template = $config['template']; + } + if (array_key_exists('layout', $config)) { + $this->_layout = $config['layout']; + } + + $this->transportClass()->config($config); + } + + /** + * Return the transport class + * + * @return AbstractTransport + * @throws SocketException + */ + public function transportClass() + { + if ($this->_transportClass) { + return $this->_transportClass; + } + list($plugin, $transportClassname) = pluginSplit($this->_transportName, true); + $transportClassname .= 'Transport'; + App::uses($transportClassname, $plugin . 'Network/Email'); + if (!class_exists($transportClassname)) { + throw new SocketException(__d('cake_dev', 'Class "%s" not found.', $transportClassname)); + } else if (!method_exists($transportClassname, 'send')) { + throw new SocketException(__d('cake_dev', 'The "%s" does not have a %s method.', $transportClassname, 'send()')); + } + + return $this->_transportClass = new $transportClassname(); + } + + /** + * Static method to fast create an instance of CakeEmail + * + * @param string|array $to Address to send (see CakeEmail::to()). If null, will try to use 'to' from transport config + * @param string $subject String of subject or null to use 'subject' from transport config + * @param string|array $message String with message or array with variables to be used in render + * @param string|array $transportConfig String to use config from EmailConfig or array with configs + * @param bool $send Send the email or just return the instance pre-configured + * @return self Instance of CakeEmail + * @throws SocketException + */ + public static function deliver($to = null, $subject = null, $message = null, $transportConfig = 'fast', $send = true) + { + $class = get_called_class(); + /** @var CakeEmail $instance */ + $instance = new $class($transportConfig); + if ($to !== null) { + $instance->to($to); + } + if ($subject !== null) { + $instance->subject($subject); + } + if (is_array($message)) { + $instance->viewVars($message); + $message = null; + } else if ($message === null && array_key_exists('message', $config = $instance->config())) { + $message = $config['message']; + } + + if ($send === true) { + $instance->send($message); + } + + return $instance; + } + + /** + * To + * + * @param string|array $email Null to get, String with email, + * Array with email as key, name as value or email as value (without name) + * @param string $name Name + * @return array|self + */ + public function to($email = null, $name = null) + { + if ($email === null) { + return $this->_to; + } + return $this->_setEmail('_to', $email, $name); + } + + /** + * Get/Set Subject. + * + * @param string $subject Subject string. + * @return string|self + */ + public function subject($subject = null) + { + if ($subject === null) { + return $this->_subject; + } + $this->_subject = $this->_encode((string)$subject); + return $this; + } + + /** + * Variables to be set on render + * + * @param array $viewVars Variables to set for view. + * @return array|self + */ + public function viewVars($viewVars = null) + { + if ($viewVars === null) { + return $this->_viewVars; + } + $this->_viewVars = array_merge($this->_viewVars, (array)$viewVars); + return $this; + } + + /** + * Send an email using the specified content, template and layout + * + * @param string|array $content String with message or array with messages + * @return array + * @throws SocketException + */ + public function send($content = null) + { + if (empty($this->_from)) { + throw new SocketException(__d('cake_dev', 'From is not specified.')); + } + if (empty($this->_to) && empty($this->_cc) && empty($this->_bcc)) { + throw new SocketException(__d('cake_dev', 'You need to specify at least one destination for to, cc or bcc.')); + } + + if (is_array($content)) { + $content = implode("\n", $content) . "\n"; + } + + $this->_message = $this->_render($this->_wrap($content)); + + $contents = $this->transportClass()->send($this); + if (!empty($this->_config['log'])) { + $config = [ + 'level' => LOG_DEBUG, + 'scope' => 'email' + ]; + if ($this->_config['log'] !== true) { + if (!is_array($this->_config['log'])) { + $this->_config['log'] = ['level' => $this->_config['log']]; + } + $config = $this->_config['log'] + $config; + } + CakeLog::write( + $config['level'], + PHP_EOL . $contents['headers'] . PHP_EOL . PHP_EOL . $contents['message'], + $config['scope'] + ); + } + return $contents; + } + + /** + * Render the body of the email. + * + * @param array $content Content to render + * @return array Email body ready to be sent + */ + protected function _render($content) + { + $this->_textMessage = $this->_htmlMessage = ''; + + $content = implode("\n", $content); + $rendered = $this->_renderTemplates($content); + + $this->_createBoundary(); + $msg = []; + + $contentIds = array_filter((array)Hash::extract($this->_attachments, '{s}.contentId')); + $hasInlineAttachments = count($contentIds) > 0; + $hasAttachments = !empty($this->_attachments); + $hasMultipleTypes = count($rendered) > 1; + $multiPart = ($hasAttachments || $hasMultipleTypes); + + $boundary = $relBoundary = $textBoundary = $this->_boundary; + + if ($hasInlineAttachments) { + $msg[] = '--' . $boundary; + $msg[] = 'Content-Type: multipart/related; boundary="rel-' . $boundary . '"'; + $msg[] = ''; + $relBoundary = $textBoundary = 'rel-' . $boundary; + } + + if ($hasMultipleTypes && $hasAttachments) { + $msg[] = '--' . $relBoundary; + $msg[] = 'Content-Type: multipart/alternative; boundary="alt-' . $boundary . '"'; + $msg[] = ''; + $textBoundary = 'alt-' . $boundary; + } + + if (isset($rendered['text'])) { + if ($multiPart) { + $msg[] = '--' . $textBoundary; + $msg[] = 'Content-Type: text/plain; charset=' . $this->_getContentTypeCharset(); + $msg[] = 'Content-Transfer-Encoding: ' . $this->_getContentTransferEncoding(); + $msg[] = ''; + } + $this->_textMessage = $rendered['text']; + $content = explode("\n", $this->_textMessage); + $msg = array_merge($msg, $content); + $msg[] = ''; + } + + if (isset($rendered['html'])) { + if ($multiPart) { + $msg[] = '--' . $textBoundary; + $msg[] = 'Content-Type: text/html; charset=' . $this->_getContentTypeCharset(); + $msg[] = 'Content-Transfer-Encoding: ' . $this->_getContentTransferEncoding(); + $msg[] = ''; + } + $this->_htmlMessage = $rendered['html']; + $content = explode("\n", $this->_htmlMessage); + $msg = array_merge($msg, $content); + $msg[] = ''; + } + + if ($textBoundary !== $relBoundary) { + $msg[] = '--' . $textBoundary . '--'; + $msg[] = ''; + } + + if ($hasInlineAttachments) { + $attachments = $this->_attachInlineFiles($relBoundary); + $msg = array_merge($msg, $attachments); + $msg[] = ''; + $msg[] = '--' . $relBoundary . '--'; + $msg[] = ''; + } + + if ($hasAttachments) { + $attachments = $this->_attachFiles($boundary); + $msg = array_merge($msg, $attachments); + } + if ($hasAttachments || $hasMultipleTypes) { + $msg[] = ''; + $msg[] = '--' . $boundary . '--'; + $msg[] = ''; + } + return $msg; + } + + /** + * Build and set all the view properties needed to render the templated emails. + * If there is no template set, the $content will be returned in a hash + * of the text content types for the email. + * + * @param string $content The content passed in from send() in most cases. + * @return array The rendered content with html and text keys. + */ + protected function _renderTemplates($content) + { + $types = $this->_getTypes(); + $rendered = []; + if (empty($this->_template)) { + foreach ($types as $type) { + $rendered[$type] = $this->_encodeString($content, $this->charset); + } + return $rendered; + } + $viewClass = $this->_viewRender; + if ($viewClass !== 'View') { + list($plugin, $viewClass) = pluginSplit($viewClass, true); + $viewClass .= 'View'; + App::uses($viewClass, $plugin . 'View'); + } + + /** @var View $View */ + $View = new $viewClass(null); + $View->viewVars = $this->_viewVars; + $View->helpers = $this->_helpers; + + if ($this->_theme) { + $View->theme = $this->_theme; + } + + $View->loadHelpers(); + + list($templatePlugin, $template) = pluginSplit($this->_template); + list($layoutPlugin, $layout) = pluginSplit($this->_layout); + if ($templatePlugin) { + $View->plugin = $templatePlugin; + } else if ($layoutPlugin) { + $View->plugin = $layoutPlugin; + } + + if ($View->get('content') === null) { + $View->set('content', $content); + } + + // Convert null to false, as View needs false to disable + // the layout. + if ($this->_layout === null) { + $this->_layout = false; + } + + foreach ($types as $type) { + $View->hasRendered = false; + $View->viewPath = $View->layoutPath = 'Emails' . DS . $type; + + $render = $View->render($this->_template, $this->_layout); + $render = str_replace(["\r\n", "\r"], "\n", $render); + $rendered[$type] = $this->_encodeString($render, $this->charset); + } + + foreach ($rendered as $type => $content) { + $rendered[$type] = $this->_wrap($content); + $rendered[$type] = implode("\n", $rendered[$type]); + $rendered[$type] = rtrim($rendered[$type], "\n"); + } + return $rendered; + } + + /** + * Gets the text body types that are in this email message + * + * @return array Array of types. Valid types are 'text' and 'html' + */ + protected function _getTypes() + { + $types = [$this->_emailFormat]; + if ($this->_emailFormat === 'both') { + $types = ['html', 'text']; + } + return $types; + } + + /** + * Translates a string for one charset to another if the App.encoding value + * differs and the mb_convert_encoding function exists + * + * @param string $text The text to be converted + * @param string $charset the target encoding + * @return string + */ + protected function _encodeString($text, $charset) + { + if ($this->_appCharset === $charset || !function_exists('mb_convert_encoding')) { + return $text; + } + return mb_convert_encoding($text, $charset, $this->_appCharset); + } + + /** + * Wrap the message to follow the RFC 2822 - 2.1.1 + * + * @param string $message Message to wrap + * @param int $wrapLength The line length + * @return array Wrapped message + */ + protected function _wrap($message, $wrapLength = CakeEmail::LINE_LENGTH_MUST) + { + if (strlen($message) === 0) { + return ['']; + } + $message = str_replace(["\r\n", "\r"], "\n", $message); + $lines = explode("\n", $message); + $formatted = []; + $cut = ($wrapLength == CakeEmail::LINE_LENGTH_MUST); + + foreach ($lines as $line) { + if (empty($line) && $line !== '0') { + $formatted[] = ''; + continue; + } + if (strlen($line) < $wrapLength) { + $formatted[] = $line; + continue; + } + if (!preg_match('/<[a-z]+.*>/i', $line)) { + $formatted = array_merge( + $formatted, + explode("\n", wordwrap($line, $wrapLength, "\n", $cut)) + ); + continue; + } + + $tagOpen = false; + $tmpLine = $tag = ''; + $tmpLineLength = 0; + for ($i = 0, $count = strlen($line); $i < $count; $i++) { + $char = $line[$i]; + if ($tagOpen) { + $tag .= $char; + if ($char === '>') { + $tagLength = strlen($tag); + if ($tagLength + $tmpLineLength < $wrapLength) { + $tmpLine .= $tag; + $tmpLineLength += $tagLength; + } else { + if ($tmpLineLength > 0) { + $formatted = array_merge( + $formatted, + explode("\n", wordwrap(trim($tmpLine), $wrapLength, "\n", $cut)) + ); + $tmpLine = ''; + $tmpLineLength = 0; + } + if ($tagLength > $wrapLength) { + $formatted[] = $tag; + } else { + $tmpLine = $tag; + $tmpLineLength = $tagLength; + } + } + $tag = ''; + $tagOpen = false; + } + continue; + } + if ($char === '<') { + $tagOpen = true; + $tag = '<'; + continue; + } + if ($char === ' ' && $tmpLineLength >= $wrapLength) { + $formatted[] = $tmpLine; + $tmpLineLength = 0; + continue; + } + $tmpLine .= $char; + $tmpLineLength++; + if ($tmpLineLength === $wrapLength) { + $nextChar = isset($line[$i + 1]) ? $line[$i + 1] : ''; + if ($nextChar === ' ' || $nextChar === '<') { + $formatted[] = trim($tmpLine); + $tmpLine = ''; + $tmpLineLength = 0; + if ($nextChar === ' ') { + $i++; + } + } else { + $lastSpace = strrpos($tmpLine, ' '); + if ($lastSpace === false) { + continue; + } + $formatted[] = trim(substr($tmpLine, 0, $lastSpace)); + $tmpLine = substr($tmpLine, $lastSpace + 1); + + $tmpLineLength = strlen($tmpLine); + } + } + } + if (!empty($tmpLine)) { + $formatted[] = $tmpLine; + } + } + $formatted[] = ''; + return $formatted; + } + + /** + * Create unique boundary identifier + * + * @return void + */ + protected function _createBoundary() + { + if (!empty($this->_attachments) || $this->_emailFormat === 'both') { + $this->_boundary = md5(uniqid(time())); + } + } + + /** + * Attach inline/embedded files to the message. + * + * @param string $boundary Boundary to use. If null, will default to $this->_boundary + * @return array An array of lines to add to the message + */ + protected function _attachInlineFiles($boundary = null) + { + if ($boundary === null) { + $boundary = $this->_boundary; + } + + $msg = []; + foreach ($this->_attachments as $filename => $fileInfo) { + if (empty($fileInfo['contentId'])) { + continue; + } + $data = isset($fileInfo['data']) ? $fileInfo['data'] : $this->_readFile($fileInfo['file']); + + $msg[] = '--' . $boundary; + $msg[] = 'Content-Type: ' . $fileInfo['mimetype']; + $msg[] = 'Content-Transfer-Encoding: base64'; + $msg[] = 'Content-ID: <' . $fileInfo['contentId'] . '>'; + $msg[] = 'Content-Disposition: inline; filename="' . $filename . '"'; + $msg[] = ''; + $msg[] = $data; + $msg[] = ''; + } + return $msg; + } + + /** + * Read the file contents and return a base64 version of the file contents. + * + * @param string $path The absolute path to the file to read. + * @return string File contents in base64 encoding + */ + protected function _readFile($path) + { + $File = new File($path); + return chunk_split(base64_encode($File->read())); + } + + /** + * Attach non-embedded files by adding file contents inside boundaries. + * + * @param string $boundary Boundary to use. If null, will default to $this->_boundary + * @return array An array of lines to add to the message + */ + protected function _attachFiles($boundary = null) + { + if ($boundary === null) { + $boundary = $this->_boundary; + } + + $msg = []; + foreach ($this->_attachments as $filename => $fileInfo) { + if (!empty($fileInfo['contentId'])) { + continue; + } + $data = isset($fileInfo['data']) ? $fileInfo['data'] : $this->_readFile($fileInfo['file']); + + $msg[] = '--' . $boundary; + $msg[] = 'Content-Type: ' . $fileInfo['mimetype']; + $msg[] = 'Content-Transfer-Encoding: base64'; + if (!isset($fileInfo['contentDisposition']) || + $fileInfo['contentDisposition'] + ) { + $msg[] = 'Content-Disposition: attachment; filename="' . $filename . '"'; + } + $msg[] = ''; + $msg[] = $data; + $msg[] = ''; + } + return $msg; + } + + /** + * From + * + * @param string|array $email Null to get, String with email, + * Array with email as key, name as value or email as value (without name) + * @param string $name Name + * @return array|CakeEmail + * @throws SocketException + */ + public function from($email = null, $name = null) + { + if ($email === null) { + return $this->_from; + } + return $this->_setEmailSingle('_from', $email, $name, __d('cake_dev', 'From requires only 1 email address.')); + } + + /** + * Set only 1 email + * + * @param string $varName Property name + * @param string|array $email String with email, + * Array with email as key, name as value or email as value (without name) + * @param string $name Name + * @param string $throwMessage Exception message + * @return self + * @throws SocketException + */ + protected function _setEmailSingle($varName, $email, $name, $throwMessage) + { + $current = $this->{$varName}; + $this->_setEmail($varName, $email, $name); + if (count($this->{$varName}) !== 1) { + $this->{$varName} = $current; + throw new SocketException($throwMessage); + } + return $this; + } + + /** + * Set email + * + * @param string $varName Property name + * @param string|array $email String with email, + * Array with email as key, name as value or email as value (without name) + * @param string $name Name + * @return self + */ + protected function _setEmail($varName, $email, $name) + { + if (!is_array($email)) { + $this->_validateEmail($email, $varName); + if ($name === null) { + $name = $email; + } + $this->{$varName} = [$email => $name]; + return $this; + } + $list = []; + foreach ($email as $key => $value) { + if (is_int($key)) { + $key = $value; + } + $this->_validateEmail($key, $varName); + $list[$key] = $value; + } + $this->{$varName} = $list; + return $this; + } + + /** + * Validate email address + * + * @param string $email Email address to validate + * @param string $context Which property was set + * @return void + * @throws SocketException If email address does not validate + */ + protected function _validateEmail($email, $context) + { + if ($this->_emailPattern === null) { + if (filter_var($email, FILTER_VALIDATE_EMAIL)) { + return; + } + } else if (preg_match($this->_emailPattern, $email)) { + return; + } + if ($email == '') { + throw new SocketException(__d('cake_dev', 'The email set for "%s" is empty.', $context)); + } + throw new SocketException(__d('cake_dev', 'Invalid email set for "%s". You passed "%s".', $context, $email)); + } + + /** + * Sender + * + * @param string|array $email Null to get, String with email, + * Array with email as key, name as value or email as value (without name) + * @param string $name Name + * @return array|CakeEmail + * @throws SocketException + */ + public function sender($email = null, $name = null) + { + if ($email === null) { + return $this->_sender; + } + return $this->_setEmailSingle('_sender', $email, $name, __d('cake_dev', 'Sender requires only 1 email address.')); + } + + /** + * Reply-To + * + * @param string|array $email Null to get, String with email, + * Array with email as key, name as value or email as value (without name) + * @param string $name Name + * @return array|CakeEmail + * @throws SocketException + */ + public function replyTo($email = null, $name = null) + { + if ($email === null) { + return $this->_replyTo; + } + return $this->_setEmailSingle('_replyTo', $email, $name, __d('cake_dev', 'Reply-To requires only 1 email address.')); + } + + /** + * Read Receipt (Disposition-Notification-To header) + * + * @param string|array $email Null to get, String with email, + * Array with email as key, name as value or email as value (without name) + * @param string $name Name + * @return array|CakeEmail + * @throws SocketException + */ + public function readReceipt($email = null, $name = null) + { + if ($email === null) { + return $this->_readReceipt; + } + return $this->_setEmailSingle('_readReceipt', $email, $name, __d('cake_dev', 'Disposition-Notification-To requires only 1 email address.')); + } + + /** + * Return Path + * + * @param string|array $email Null to get, String with email, + * Array with email as key, name as value or email as value (without name) + * @param string $name Name + * @return array|CakeEmail + * @throws SocketException + */ + public function returnPath($email = null, $name = null) + { + if ($email === null) { + return $this->_returnPath; + } + return $this->_setEmailSingle('_returnPath', $email, $name, __d('cake_dev', 'Return-Path requires only 1 email address.')); + } + + /** + * Add To + * + * @param string|array $email Null to get, String with email, + * Array with email as key, name as value or email as value (without name) + * @param string $name Name + * @return self + */ + public function addTo($email, $name = null) + { + return $this->_addEmail('_to', $email, $name); + } + + /** + * Add email + * + * @param string $varName Property name + * @param string|array $email String with email, + * Array with email as key, name as value or email as value (without name) + * @param string $name Name + * @return self + * @throws SocketException + */ + protected function _addEmail($varName, $email, $name) + { + if (!is_array($email)) { + $this->_validateEmail($email, $varName); + if ($name === null) { + $name = $email; + } + $this->{$varName}[$email] = $name; + return $this; + } + $list = []; + foreach ($email as $key => $value) { + if (is_int($key)) { + $key = $value; + } + $this->_validateEmail($key, $varName); + $list[$key] = $value; + } + $this->{$varName} = array_merge($this->{$varName}, $list); + return $this; + } + + /** + * Cc + * + * @param string|array $email Null to get, String with email, + * Array with email as key, name as value or email as value (without name) + * @param string $name Name + * @return array|self + */ + public function cc($email = null, $name = null) + { + if ($email === null) { + return $this->_cc; + } + return $this->_setEmail('_cc', $email, $name); + } + + /** + * Add Cc + * + * @param string|array $email Null to get, String with email, + * Array with email as key, name as value or email as value (without name) + * @param string $name Name + * @return self + */ + public function addCc($email, $name = null) + { + return $this->_addEmail('_cc', $email, $name); + } + + /** + * Bcc + * + * @param string|array $email Null to get, String with email, + * Array with email as key, name as value or email as value (without name) + * @param string $name Name + * @return array|self + */ + public function bcc($email = null, $name = null) + { + if ($email === null) { + return $this->_bcc; + } + return $this->_setEmail('_bcc', $email, $name); + } + + /** + * Add Bcc + * + * @param string|array $email Null to get, String with email, + * Array with email as key, name as value or email as value (without name) + * @param string $name Name + * @return self + */ + public function addBcc($email, $name = null) + { + return $this->_addEmail('_bcc', $email, $name); + } + + /** + * Charset setter/getter + * + * @param string $charset Character set. + * @return string this->charset + */ + public function charset($charset = null) + { + if ($charset === null) { + return $this->charset; + } + $this->charset = $charset; + if (empty($this->headerCharset)) { + $this->headerCharset = $charset; + } + return $this->charset; + } + + /** + * HeaderCharset setter/getter + * + * @param string $charset Character set. + * @return string this->charset + */ + public function headerCharset($charset = null) + { + if ($charset === null) { + return $this->headerCharset; + } + return $this->headerCharset = $charset; + } + + /** + * EmailPattern setter/getter + * + * @param string|bool|null $regex The pattern to use for email address validation, + * null to unset the pattern and make use of filter_var() instead, false or + * nothing to return the current value + * @return string|self + */ + public function emailPattern($regex = false) + { + if ($regex === false) { + return $this->_emailPattern; + } + $this->_emailPattern = $regex; + return $this; + } + + /** + * Add header for the message + * + * @param array $headers Headers to set. + * @return self + * @throws SocketException + */ + public function addHeaders($headers) + { + if (!is_array($headers)) { + throw new SocketException(__d('cake_dev', '$headers should be an array.')); + } + $this->_headers = array_merge($this->_headers, $headers); + return $this; + } + + /** + * Get list of headers + * + * ### Includes: + * + * - `from` + * - `replyTo` + * - `readReceipt` + * - `returnPath` + * - `to` + * - `cc` + * - `bcc` + * - `subject` + * + * @param array $include List of headers. + * @return array + */ + public function getHeaders($include = []) + { + if ($include == array_values($include)) { + $include = array_fill_keys($include, true); + } + $defaults = array_fill_keys( + [ + 'from', 'sender', 'replyTo', 'readReceipt', 'returnPath', + 'to', 'cc', 'bcc', 'subject'], + false + ); + $include += $defaults; + + $headers = []; + $relation = [ + 'from' => 'From', + 'replyTo' => 'Reply-To', + 'readReceipt' => 'Disposition-Notification-To', + 'returnPath' => 'Return-Path' + ]; + foreach ($relation as $var => $header) { + if ($include[$var]) { + $var = '_' . $var; + $headers[$header] = current($this->_formatAddress($this->{$var})); + } + } + if ($include['sender']) { + if (key($this->_sender) === key($this->_from)) { + $headers['Sender'] = ''; + } else { + $headers['Sender'] = current($this->_formatAddress($this->_sender)); + } + } + + foreach (['to', 'cc', 'bcc'] as $var) { + if ($include[$var]) { + $classVar = '_' . $var; + $headers[ucfirst($var)] = implode(', ', $this->_formatAddress($this->{$classVar})); + } + } + + $headers += $this->_headers; + if (!isset($headers['X-Mailer'])) { + $headers['X-Mailer'] = static::EMAIL_CLIENT; + } + if (!isset($headers['Date'])) { + $headers['Date'] = date(DATE_RFC2822); + } + if ($this->_messageId !== false) { + if ($this->_messageId === true) { + $headers['Message-ID'] = '<' . str_replace('-', '', CakeText::uuid()) . '@' . $this->_domain . '>'; + } else { + $headers['Message-ID'] = $this->_messageId; + } + } + + if ($include['subject']) { + $headers['Subject'] = $this->_subject; + } + + $headers['MIME-Version'] = '1.0'; + if (!empty($this->_attachments)) { + $headers['Content-Type'] = 'multipart/mixed; boundary="' . $this->_boundary . '"'; + } else if ($this->_emailFormat === 'both') { + $headers['Content-Type'] = 'multipart/alternative; boundary="' . $this->_boundary . '"'; + } else if ($this->_emailFormat === 'text') { + $headers['Content-Type'] = 'text/plain; charset=' . $this->_getContentTypeCharset(); + } else if ($this->_emailFormat === 'html') { + $headers['Content-Type'] = 'text/html; charset=' . $this->_getContentTypeCharset(); + } + $headers['Content-Transfer-Encoding'] = $this->_getContentTransferEncoding(); + + return $headers; + } + + /** + * Sets headers for the message + * + * @param array $headers Associative array containing headers to be set. + * @return self + * @throws SocketException + */ + public function setHeaders($headers) + { + if (!is_array($headers)) { + throw new SocketException(__d('cake_dev', '$headers should be an array.')); + } + $this->_headers = $headers; + return $this; + } + + /** + * Format addresses + * + * If the address contains non alphanumeric/whitespace characters, it will + * be quoted as characters like `:` and `,` are known to cause issues + * in address header fields. + * + * @param array $address Addresses to format. + * @return array + */ + protected function _formatAddress($address) + { + $return = []; + foreach ($address as $email => $alias) { + if ($email === $alias) { + $return[] = $email; + } else { + $encoded = $this->_encode($alias); + if ( + $encoded === $alias && preg_match('/[^a-z0-9 ]/i', $encoded) || + strpos($encoded, ',') !== false + ) { + $encoded = '"' . str_replace('"', '\"', $encoded) . '"'; + } + $return[] = sprintf('%s <%s>', $encoded, $email); + } + } + return $return; + } + + /** + * Encode the specified string using the current charset + * + * @param string $text String to encode + * @return string Encoded string + */ + protected function _encode($text) + { + $internalEncoding = function_exists('mb_internal_encoding'); + if ($internalEncoding) { + $restore = mb_internal_encoding(); + mb_internal_encoding($this->_appCharset); + } + if (empty($this->headerCharset)) { + $this->headerCharset = $this->charset; + } + $return = mb_encode_mimeheader($text, $this->headerCharset, 'B'); + if ($internalEncoding) { + mb_internal_encoding($restore); + } + return $return; + } + + /** + * Return charset value for Content-Type. + * + * Checks fallback/compatibility types which include workarounds + * for legacy japanese character sets. + * + * @return string + */ + protected function _getContentTypeCharset() + { + $charset = strtoupper($this->charset); + if (array_key_exists($charset, $this->_contentTypeCharset)) { + return strtoupper($this->_contentTypeCharset[$charset]); + } + return strtoupper($this->charset); + } + + /** + * Return the Content-Transfer Encoding value based on the set charset + * + * @return string + */ + protected function _getContentTransferEncoding() + { + $charset = strtoupper($this->charset); + if (in_array($charset, $this->_charset8bit)) { + return '8bit'; + } + return '7bit'; + } + + /** + * Template and layout + * + * @param bool|string $template Template name or null to not use + * @param bool|string $layout Layout name or null to not use + * @return array|self + */ + public function template($template = false, $layout = false) + { + if ($template === false) { + return [ + 'template' => $this->_template, + 'layout' => $this->_layout + ]; + } + $this->_template = $template; + if ($layout !== false) { + $this->_layout = $layout; + } + return $this; + } + + /** + * View class for render + * + * @param string $viewClass View class name. + * @return string|self + */ + public function viewRender($viewClass = null) + { + if ($viewClass === null) { + return $this->_viewRender; + } + $this->_viewRender = $viewClass; + return $this; + } + + /** + * Theme to use when rendering + * + * @param string $theme Theme name. + * @return string|self + */ + public function theme($theme = null) + { + if ($theme === null) { + return $this->_theme; + } + $this->_theme = $theme; + return $this; + } + + /** + * Helpers to be used in render + * + * @param array $helpers Helpers list. + * @return array|self + */ + public function helpers($helpers = null) + { + if ($helpers === null) { + return $this->_helpers; + } + $this->_helpers = (array)$helpers; + return $this; + } + + /** + * Email format + * + * @param string $format Formatting string. + * @return string|self + * @throws SocketException + */ + public function emailFormat($format = null) + { + if ($format === null) { + return $this->_emailFormat; + } + if (!in_array($format, $this->_emailFormatAvailable)) { + throw new SocketException(__d('cake_dev', 'Format not available.')); + } + $this->_emailFormat = $format; + return $this; + } + + /** + * Transport name + * + * @param string $name Transport name. + * @return string|self + */ + public function transport($name = null) + { + if ($name === null) { + return $this->_transportName; + } + $this->_transportName = (string)$name; + $this->_transportClass = null; + return $this; + } + + /** + * Message-ID + * + * @param bool|string $message True to generate a new Message-ID, False to ignore (not send in email), String to set as Message-ID + * @return bool|string|self + * @throws SocketException + */ + public function messageId($message = null) + { + if ($message === null) { + return $this->_messageId; + } + if (is_bool($message)) { + $this->_messageId = $message; + } else { + if (!preg_match('/^\<.+@.+\>$/', $message)) { + throw new SocketException(__d('cake_dev', 'Invalid format for Message-ID. The text should be something like ""')); + } + $this->_messageId = $message; + } + return $this; + } + + /** + * Domain as top level (the part after @) + * + * @param string $domain Manually set the domain for CLI mailing + * @return string|self + */ + public function domain($domain = null) + { + if ($domain === null) { + return $this->_domain; + } + $this->_domain = $domain; + return $this; + } + + /** + * Add attachments + * + * @param string|array $attachments String with the filename or array with filenames + * @return self + * @throws SocketException + * @see CakeEmail::attachments() + */ + public function addAttachments($attachments) + { + $current = $this->_attachments; + $this->attachments($attachments); + $this->_attachments = array_merge($current, $this->_attachments); + return $this; + } + + /** + * Add attachments to the email message + * + * Attachments can be defined in a few forms depending on how much control you need: + * + * Attach a single file: + * + * ``` + * $email->attachments('path/to/file'); + * ``` + * + * Attach a file with a different filename: + * + * ``` + * $email->attachments(array('custom_name.txt' => 'path/to/file.txt')); + * ``` + * + * Attach a file and specify additional properties: + * + * ``` + * $email->attachments(array('custom_name.png' => array( + * 'file' => 'path/to/file', + * 'mimetype' => 'image/png', + * 'contentId' => 'abc123', + * 'contentDisposition' => false + * )); + * ``` + * + * Attach a file from string and specify additional properties: + * + * ``` + * $email->attachments(array('custom_name.png' => array( + * 'data' => file_get_contents('path/to/file'), + * 'mimetype' => 'image/png' + * )); + * ``` + * + * The `contentId` key allows you to specify an inline attachment. In your email text, you + * can use `` to display the image inline. + * + * The `contentDisposition` key allows you to disable the `Content-Disposition` header, this can improve + * attachment compatibility with outlook email clients. + * + * @param string|array $attachments String with the filename or array with filenames + * @return array|self Either the array of attachments when getting or $this when setting. + * @throws SocketException + */ + public function attachments($attachments = null) + { + if ($attachments === null) { + return $this->_attachments; + } + $attach = []; + foreach ((array)$attachments as $name => $fileInfo) { + if (!is_array($fileInfo)) { + $fileInfo = ['file' => $fileInfo]; + } + if (!isset($fileInfo['file'])) { + if (!isset($fileInfo['data'])) { + throw new SocketException(__d('cake_dev', 'No file or data specified.')); + } + if (is_int($name)) { + throw new SocketException(__d('cake_dev', 'No filename specified.')); + } + $fileInfo['data'] = chunk_split(base64_encode($fileInfo['data']), 76, "\r\n"); + } else { + $fileName = $fileInfo['file']; + $fileInfo['file'] = realpath($fileInfo['file']); + if ($fileInfo['file'] === false || !file_exists($fileInfo['file'])) { + throw new SocketException(__d('cake_dev', 'File not found: "%s"', $fileName)); + } + if (is_int($name)) { + $name = basename($fileInfo['file']); + } + } + if (!isset($fileInfo['mimetype']) && isset($fileInfo['file']) && function_exists('mime_content_type')) { + $fileInfo['mimetype'] = mime_content_type($fileInfo['file']); + } + if (!isset($fileInfo['mimetype'])) { + $fileInfo['mimetype'] = 'application/octet-stream'; + } + $attach[$name] = $fileInfo; + } + $this->_attachments = $attach; + return $this; + } + + /** + * Get generated message (used by transport classes) + * + * @param string $type Use MESSAGE_* constants or null to return the full message as array + * @return string|array String if have type, array if type is null + */ + public function message($type = null) + { + switch ($type) { + case static::MESSAGE_HTML: + return $this->_htmlMessage; + case static::MESSAGE_TEXT: + return $this->_textMessage; + } + return $this->_message; + } + + /** + * Reset all CakeEmail internal variables to be able to send out a new email. + * + * @return self + */ + public function reset() + { + $this->_to = []; + $this->_from = []; + $this->_sender = []; + $this->_replyTo = []; + $this->_readReceipt = []; + $this->_returnPath = []; + $this->_cc = []; + $this->_bcc = []; + $this->_messageId = true; + $this->_subject = ''; + $this->_headers = []; + $this->_layout = 'default'; + $this->_template = ''; + $this->_viewRender = 'View'; + $this->_viewVars = []; + $this->_theme = null; + $this->_helpers = ['Html']; + $this->_textMessage = ''; + $this->_htmlMessage = ''; + $this->_message = ''; + $this->_emailFormat = 'text'; + $this->_transportName = 'Mail'; + $this->_transportClass = null; + $this->charset = 'utf-8'; + $this->headerCharset = null; + $this->_attachments = []; + $this->_config = []; + $this->_emailPattern = static::EMAIL_PATTERN; + return $this; + } } diff --git a/lib/Cake/Network/Email/DebugTransport.php b/lib/Cake/Network/Email/DebugTransport.php index 9c7d2cdd..e235adbf 100755 --- a/lib/Cake/Network/Email/DebugTransport.php +++ b/lib/Cake/Network/Email/DebugTransport.php @@ -23,19 +23,21 @@ * * @package Cake.Network.Email */ -class DebugTransport extends AbstractTransport { +class DebugTransport extends AbstractTransport +{ -/** - * Send mail - * - * @param CakeEmail $email CakeEmail - * @return array - */ - public function send(CakeEmail $email) { - $headers = $email->getHeaders(array('from', 'sender', 'replyTo', 'readReceipt', 'returnPath', 'to', 'cc', 'subject')); - $headers = $this->_headersToString($headers); - $message = implode("\r\n", (array)$email->message()); - return array('headers' => $headers, 'message' => $message); - } + /** + * Send mail + * + * @param CakeEmail $email CakeEmail + * @return array + */ + public function send(CakeEmail $email) + { + $headers = $email->getHeaders(['from', 'sender', 'replyTo', 'readReceipt', 'returnPath', 'to', 'cc', 'subject']); + $headers = $this->_headersToString($headers); + $message = implode("\r\n", (array)$email->message()); + return ['headers' => $headers, 'message' => $message]; + } } diff --git a/lib/Cake/Network/Email/MailTransport.php b/lib/Cake/Network/Email/MailTransport.php index 36782b2f..beefb9de 100755 --- a/lib/Cake/Network/Email/MailTransport.php +++ b/lib/Cake/Network/Email/MailTransport.php @@ -22,65 +22,68 @@ * * @package Cake.Network.Email */ -class MailTransport extends AbstractTransport { +class MailTransport extends AbstractTransport +{ -/** - * Send mail - * - * @param CakeEmail $email CakeEmail - * @return array - * @throws SocketException When mail cannot be sent. - */ - public function send(CakeEmail $email) { - $eol = PHP_EOL; - if (isset($this->_config['eol'])) { - $eol = $this->_config['eol']; - } - $headers = $email->getHeaders(array('from', 'sender', 'replyTo', 'readReceipt', 'returnPath', 'to', 'cc', 'bcc')); - $to = $headers['To']; - unset($headers['To']); - foreach ($headers as $key => $header) { - $headers[$key] = str_replace(array("\r", "\n"), '', $header); - } - $headers = $this->_headersToString($headers, $eol); - $subject = str_replace(array("\r", "\n"), '', $email->subject()); - $to = str_replace(array("\r", "\n"), '', $to); + /** + * Send mail + * + * @param CakeEmail $email CakeEmail + * @return array + * @throws SocketException When mail cannot be sent. + */ + public function send(CakeEmail $email) + { + $eol = PHP_EOL; + if (isset($this->_config['eol'])) { + $eol = $this->_config['eol']; + } + $headers = $email->getHeaders(['from', 'sender', 'replyTo', 'readReceipt', 'returnPath', 'to', 'cc', 'bcc']); + $to = $headers['To']; + unset($headers['To']); + foreach ($headers as $key => $header) { + $headers[$key] = str_replace(["\r", "\n"], '', $header); + } + $headers = $this->_headersToString($headers, $eol); + $subject = str_replace(["\r", "\n"], '', $email->subject()); + $to = str_replace(["\r", "\n"], '', $to); - $message = implode($eol, $email->message()); + $message = implode($eol, $email->message()); - $params = isset($this->_config['additionalParameters']) ? $this->_config['additionalParameters'] : null; - $this->_mail($to, $subject, $message, $headers, $params); + $params = isset($this->_config['additionalParameters']) ? $this->_config['additionalParameters'] : null; + $this->_mail($to, $subject, $message, $headers, $params); - $headers .= $eol . 'Subject: ' . $subject; - $headers .= $eol . 'To: ' . $to; - return array('headers' => $headers, 'message' => $message); - } + $headers .= $eol . 'Subject: ' . $subject; + $headers .= $eol . 'To: ' . $to; + return ['headers' => $headers, 'message' => $message]; + } -/** - * Wraps internal function mail() and throws exception instead of errors if anything goes wrong - * - * @param string $to email's recipient - * @param string $subject email's subject - * @param string $message email's body - * @param string $headers email's custom headers - * @param string $params additional params for sending email, will be ignored when in safe_mode - * @throws SocketException if mail could not be sent - * @return void - */ - protected function _mail($to, $subject, $message, $headers, $params = null) { - if (ini_get('safe_mode')) { - //@codingStandardsIgnoreStart - if (!@mail($to, $subject, $message, $headers)) { - $error = error_get_last(); - $msg = 'Could not send email: ' . (isset($error['message']) ? $error['message'] : 'unknown'); - throw new SocketException($msg); - } - } elseif (!@mail($to, $subject, $message, $headers, $params)) { - $error = error_get_last(); - $msg = 'Could not send email: ' . (isset($error['message']) ? $error['message'] : 'unknown'); - //@codingStandardsIgnoreEnd - throw new SocketException($msg); - } - } + /** + * Wraps internal function mail() and throws exception instead of errors if anything goes wrong + * + * @param string $to email's recipient + * @param string $subject email's subject + * @param string $message email's body + * @param string $headers email's custom headers + * @param string $params additional params for sending email, will be ignored when in safe_mode + * @throws SocketException if mail could not be sent + * @return void + */ + protected function _mail($to, $subject, $message, $headers, $params = null) + { + if (ini_get('safe_mode')) { + //@codingStandardsIgnoreStart + if (!@mail($to, $subject, $message, $headers)) { + $error = error_get_last(); + $msg = 'Could not send email: ' . (isset($error['message']) ? $error['message'] : 'unknown'); + throw new SocketException($msg); + } + } else if (!@mail($to, $subject, $message, $headers, $params)) { + $error = error_get_last(); + $msg = 'Could not send email: ' . (isset($error['message']) ? $error['message'] : 'unknown'); + //@codingStandardsIgnoreEnd + throw new SocketException($msg); + } + } } diff --git a/lib/Cake/Network/Email/SmtpTransport.php b/lib/Cake/Network/Email/SmtpTransport.php index d69fa390..b9ce9178 100755 --- a/lib/Cake/Network/Email/SmtpTransport.php +++ b/lib/Cake/Network/Email/SmtpTransport.php @@ -23,357 +23,375 @@ * * @package Cake.Network.Email */ -class SmtpTransport extends AbstractTransport { +class SmtpTransport extends AbstractTransport +{ -/** - * Socket to SMTP server - * - * @var CakeSocket - */ - protected $_socket; + /** + * Socket to SMTP server + * + * @var CakeSocket + */ + protected $_socket; -/** - * Content of email to return - * - * @var string - */ - protected $_content; + /** + * Content of email to return + * + * @var string + */ + protected $_content; -/** - * The response of the last sent SMTP command. - * - * @var array - */ - protected $_lastResponse = array(); + /** + * The response of the last sent SMTP command. + * + * @var array + */ + protected $_lastResponse = []; -/** - * Returns the response of the last sent SMTP command. - * - * A response consists of one or more lines containing a response - * code and an optional response message text: - * ``` - * array( - * array( - * 'code' => '250', - * 'message' => 'mail.example.com' - * ), - * array( - * 'code' => '250', - * 'message' => 'PIPELINING' - * ), - * array( - * 'code' => '250', - * 'message' => '8BITMIME' - * ), - * // etc... - * ) - * ``` - * - * @return array - */ - public function getLastResponse() { - return $this->_lastResponse; - } + /** + * Returns the response of the last sent SMTP command. + * + * A response consists of one or more lines containing a response + * code and an optional response message text: + * ``` + * array( + * array( + * 'code' => '250', + * 'message' => 'mail.example.com' + * ), + * array( + * 'code' => '250', + * 'message' => 'PIPELINING' + * ), + * array( + * 'code' => '250', + * 'message' => '8BITMIME' + * ), + * // etc... + * ) + * ``` + * + * @return array + */ + public function getLastResponse() + { + return $this->_lastResponse; + } -/** - * Send mail - * - * @param CakeEmail $email CakeEmail - * @return array - * @throws SocketException - */ - public function send(CakeEmail $email) { - $this->_connect(); - $this->_auth(); - $this->_sendRcpt($email); - $this->_sendData($email); - $this->_disconnect(); + /** + * Send mail + * + * @param CakeEmail $email CakeEmail + * @return array + * @throws SocketException + */ + public function send(CakeEmail $email) + { + $this->_connect(); + $this->_auth(); + $this->_sendRcpt($email); + $this->_sendData($email); + $this->_disconnect(); - return $this->_content; - } + return $this->_content; + } -/** - * Set the configuration - * - * @param array $config Configuration options. - * @return array Returns configs - */ - public function config($config = null) { - if ($config === null) { - return $this->_config; - } - $default = array( - 'host' => 'localhost', - 'port' => 25, - 'timeout' => 30, - 'username' => null, - 'password' => null, - 'client' => null, - 'tls' => false, - 'ssl_allow_self_signed' => false - ); - $this->_config = array_merge($default, $this->_config, $config); - return $this->_config; - } + /** + * Connect to SMTP Server + * + * @return void + * @throws SocketException + */ + protected function _connect() + { + $this->_generateSocket(); + if (!$this->_socket->connect()) { + throw new SocketException(__d('cake_dev', 'Unable to connect to SMTP server.')); + } + $this->_smtpSend(null, '220'); -/** - * Parses and stores the reponse lines in `'code' => 'message'` format. - * - * @param array $responseLines Response lines to parse. - * @return void - */ - protected function _bufferResponseLines(array $responseLines) { - $response = array(); - foreach ($responseLines as $responseLine) { - if (preg_match('/^(\d{3})(?:[ -]+(.*))?$/', $responseLine, $match)) { - $response[] = array( - 'code' => $match[1], - 'message' => isset($match[2]) ? $match[2] : null - ); - } - } - $this->_lastResponse = array_merge($this->_lastResponse, $response); - } + if (isset($this->_config['client'])) { + $host = $this->_config['client']; + } else if ($httpHost = env('HTTP_HOST')) { + list($host) = explode(':', $httpHost); + } else { + $host = 'localhost'; + } -/** - * Connect to SMTP Server - * - * @return void - * @throws SocketException - */ - protected function _connect() { - $this->_generateSocket(); - if (!$this->_socket->connect()) { - throw new SocketException(__d('cake_dev', 'Unable to connect to SMTP server.')); - } - $this->_smtpSend(null, '220'); + try { + $this->_smtpSend("EHLO {$host}", '250'); + if ($this->_config['tls']) { + $this->_smtpSend("STARTTLS", '220'); + $this->_socket->enableCrypto('tls'); + $this->_smtpSend("EHLO {$host}", '250'); + } + } catch (SocketException $e) { + if ($this->_config['tls']) { + throw new SocketException(__d('cake_dev', 'SMTP server did not accept the connection or trying to connect to non TLS SMTP server using TLS.')); + } + try { + $this->_smtpSend("HELO {$host}", '250'); + } catch (SocketException $e2) { + throw new SocketException(__d('cake_dev', 'SMTP server did not accept the connection.')); + } + } + } - if (isset($this->_config['client'])) { - $host = $this->_config['client']; - } elseif ($httpHost = env('HTTP_HOST')) { - list($host) = explode(':', $httpHost); - } else { - $host = 'localhost'; - } + /** + * Helper method to generate socket + * + * @return void + * @throws SocketException + */ + protected function _generateSocket() + { + $this->_socket = new CakeSocket($this->_config); + } - try { - $this->_smtpSend("EHLO {$host}", '250'); - if ($this->_config['tls']) { - $this->_smtpSend("STARTTLS", '220'); - $this->_socket->enableCrypto('tls'); - $this->_smtpSend("EHLO {$host}", '250'); - } - } catch (SocketException $e) { - if ($this->_config['tls']) { - throw new SocketException(__d('cake_dev', 'SMTP server did not accept the connection or trying to connect to non TLS SMTP server using TLS.')); - } - try { - $this->_smtpSend("HELO {$host}", '250'); - } catch (SocketException $e2) { - throw new SocketException(__d('cake_dev', 'SMTP server did not accept the connection.')); - } - } - } + /** + * Protected method for sending data to SMTP connection + * + * @param string|null $data Data to be sent to SMTP server + * @param string|bool $checkCode Code to check for in server response, false to skip + * @return string|null The matched code, or null if nothing matched + * @throws SocketException + */ + protected function _smtpSend($data, $checkCode = '250') + { + $this->_lastResponse = []; -/** - * Send authentication - * - * @return void - * @throws SocketException - */ - protected function _auth() { - if (isset($this->_config['username']) && isset($this->_config['password'])) { - $replyCode = $this->_smtpSend('AUTH LOGIN', '334|500|502|504'); - if ($replyCode == '334') { - try { - $this->_smtpSend(base64_encode($this->_config['username']), '334'); - } catch (SocketException $e) { - throw new SocketException(__d('cake_dev', 'SMTP server did not accept the username.')); - } - try { - $this->_smtpSend(base64_encode($this->_config['password']), '235'); - } catch (SocketException $e) { - throw new SocketException(__d('cake_dev', 'SMTP server did not accept the password.')); - } - } elseif ($replyCode == '504') { - throw new SocketException(__d('cake_dev', 'SMTP authentication method not allowed, check if SMTP server requires TLS.')); - } else { - throw new SocketException(__d('cake_dev', 'AUTH command not recognized or not implemented, SMTP server may not require authentication.')); - } - } - } + if ($data !== null) { + $this->_socket->write($data . "\r\n"); + } + while ($checkCode !== false) { + $response = ''; + $startTime = time(); + while (substr($response, -2) !== "\r\n" && ((time() - $startTime) < $this->_config['timeout'])) { + $bytes = $this->_socket->read(); + if ($bytes === false || $bytes === null) { + break; + } + $response .= $bytes; + } + if (substr($response, -2) !== "\r\n") { + throw new SocketException(__d('cake_dev', 'SMTP timeout.')); + } + $responseLines = explode("\r\n", rtrim($response, "\r\n")); + $response = end($responseLines); -/** - * Prepares the `MAIL FROM` SMTP command. - * - * @param string $email The email address to send with the command. - * @return string - */ - protected function _prepareFromCmd($email) { - return 'MAIL FROM:<' . $email . '>'; - } + $this->_bufferResponseLines($responseLines); -/** - * Prepares the `RCPT TO` SMTP command. - * - * @param string $email The email address to send with the command. - * @return string - */ - protected function _prepareRcptCmd($email) { - return 'RCPT TO:<' . $email . '>'; - } + if (preg_match('/^(' . $checkCode . ')(.)/', $response, $code)) { + if ($code[2] === '-') { + continue; + } + return $code[1]; + } + throw new SocketException(__d('cake_dev', 'SMTP Error: %s', $response)); + } + } -/** - * Prepares the `from` email address. - * - * @param CakeEmail $email CakeEmail - * @return array - */ - protected function _prepareFromAddress(CakeEmail $email) { - $from = $email->returnPath(); - if (empty($from)) { - $from = $email->from(); - } - return $from; - } + /** + * Parses and stores the reponse lines in `'code' => 'message'` format. + * + * @param array $responseLines Response lines to parse. + * @return void + */ + protected function _bufferResponseLines(array $responseLines) + { + $response = []; + foreach ($responseLines as $responseLine) { + if (preg_match('/^(\d{3})(?:[ -]+(.*))?$/', $responseLine, $match)) { + $response[] = [ + 'code' => $match[1], + 'message' => isset($match[2]) ? $match[2] : null + ]; + } + } + $this->_lastResponse = array_merge($this->_lastResponse, $response); + } -/** - * Prepares the recipient email addresses. - * - * @param CakeEmail $email CakeEmail - * @return array - */ - protected function _prepareRecipientAddresses(CakeEmail $email) { - $to = $email->to(); - $cc = $email->cc(); - $bcc = $email->bcc(); - return array_merge(array_keys($to), array_keys($cc), array_keys($bcc)); - } + /** + * Send authentication + * + * @return void + * @throws SocketException + */ + protected function _auth() + { + if (isset($this->_config['username']) && isset($this->_config['password'])) { + $replyCode = $this->_smtpSend('AUTH LOGIN', '334|500|502|504'); + if ($replyCode == '334') { + try { + $this->_smtpSend(base64_encode($this->_config['username']), '334'); + } catch (SocketException $e) { + throw new SocketException(__d('cake_dev', 'SMTP server did not accept the username.')); + } + try { + $this->_smtpSend(base64_encode($this->_config['password']), '235'); + } catch (SocketException $e) { + throw new SocketException(__d('cake_dev', 'SMTP server did not accept the password.')); + } + } else if ($replyCode == '504') { + throw new SocketException(__d('cake_dev', 'SMTP authentication method not allowed, check if SMTP server requires TLS.')); + } else { + throw new SocketException(__d('cake_dev', 'AUTH command not recognized or not implemented, SMTP server may not require authentication.')); + } + } + } -/** - * Prepares the message headers. - * - * @param CakeEmail $email CakeEmail - * @return array - */ - protected function _prepareMessageHeaders(CakeEmail $email) { - return $email->getHeaders(array('from', 'sender', 'replyTo', 'readReceipt', 'to', 'cc', 'subject')); - } + /** + * Send emails + * + * @param CakeEmail $email CakeEmail + * @return void + * @throws SocketException + */ + protected function _sendRcpt(CakeEmail $email) + { + $from = $this->_prepareFromAddress($email); + $this->_smtpSend($this->_prepareFromCmd(key($from))); -/** - * Prepares the message body. - * - * @param CakeEmail $email CakeEmail - * @return string - */ - protected function _prepareMessage(CakeEmail $email) { - $lines = $email->message(); - $messages = array(); - foreach ($lines as $line) { - if ((!empty($line)) && ($line[0] === '.')) { - $messages[] = '.' . $line; - } else { - $messages[] = $line; - } - } - return implode("\r\n", $messages); - } + $emails = $this->_prepareRecipientAddresses($email); + foreach ($emails as $email) { + $this->_smtpSend($this->_prepareRcptCmd($email)); + } + } -/** - * Send emails - * - * @param CakeEmail $email CakeEmail - * @return void - * @throws SocketException - */ - protected function _sendRcpt(CakeEmail $email) { - $from = $this->_prepareFromAddress($email); - $this->_smtpSend($this->_prepareFromCmd(key($from))); + /** + * Prepares the `from` email address. + * + * @param CakeEmail $email CakeEmail + * @return array + */ + protected function _prepareFromAddress(CakeEmail $email) + { + $from = $email->returnPath(); + if (empty($from)) { + $from = $email->from(); + } + return $from; + } - $emails = $this->_prepareRecipientAddresses($email); - foreach ($emails as $email) { - $this->_smtpSend($this->_prepareRcptCmd($email)); - } - } + /** + * Prepares the `MAIL FROM` SMTP command. + * + * @param string $email The email address to send with the command. + * @return string + */ + protected function _prepareFromCmd($email) + { + return 'MAIL FROM:<' . $email . '>'; + } -/** - * Send Data - * - * @param CakeEmail $email CakeEmail - * @return void - * @throws SocketException - */ - protected function _sendData(CakeEmail $email) { - $this->_smtpSend('DATA', '354'); + /** + * Prepares the recipient email addresses. + * + * @param CakeEmail $email CakeEmail + * @return array + */ + protected function _prepareRecipientAddresses(CakeEmail $email) + { + $to = $email->to(); + $cc = $email->cc(); + $bcc = $email->bcc(); + return array_merge(array_keys($to), array_keys($cc), array_keys($bcc)); + } - $headers = $this->_headersToString($this->_prepareMessageHeaders($email)); - $message = $this->_prepareMessage($email); + /** + * Prepares the `RCPT TO` SMTP command. + * + * @param string $email The email address to send with the command. + * @return string + */ + protected function _prepareRcptCmd($email) + { + return 'RCPT TO:<' . $email . '>'; + } - $this->_smtpSend($headers . "\r\n\r\n" . $message . "\r\n\r\n\r\n."); - $this->_content = array('headers' => $headers, 'message' => $message); - } + /** + * Send Data + * + * @param CakeEmail $email CakeEmail + * @return void + * @throws SocketException + */ + protected function _sendData(CakeEmail $email) + { + $this->_smtpSend('DATA', '354'); -/** - * Disconnect - * - * @return void - * @throws SocketException - */ - protected function _disconnect() { - $this->_smtpSend('QUIT', false); - $this->_socket->disconnect(); - } + $headers = $this->_headersToString($this->_prepareMessageHeaders($email)); + $message = $this->_prepareMessage($email); -/** - * Helper method to generate socket - * - * @return void - * @throws SocketException - */ - protected function _generateSocket() { - $this->_socket = new CakeSocket($this->_config); - } + $this->_smtpSend($headers . "\r\n\r\n" . $message . "\r\n\r\n\r\n."); + $this->_content = ['headers' => $headers, 'message' => $message]; + } -/** - * Protected method for sending data to SMTP connection - * - * @param string|null $data Data to be sent to SMTP server - * @param string|bool $checkCode Code to check for in server response, false to skip - * @return string|null The matched code, or null if nothing matched - * @throws SocketException - */ - protected function _smtpSend($data, $checkCode = '250') { - $this->_lastResponse = array(); + /** + * Prepares the message headers. + * + * @param CakeEmail $email CakeEmail + * @return array + */ + protected function _prepareMessageHeaders(CakeEmail $email) + { + return $email->getHeaders(['from', 'sender', 'replyTo', 'readReceipt', 'to', 'cc', 'subject']); + } - if ($data !== null) { - $this->_socket->write($data . "\r\n"); - } - while ($checkCode !== false) { - $response = ''; - $startTime = time(); - while (substr($response, -2) !== "\r\n" && ((time() - $startTime) < $this->_config['timeout'])) { - $bytes = $this->_socket->read(); - if ($bytes === false || $bytes === null) { - break; - } - $response .= $bytes; - } - if (substr($response, -2) !== "\r\n") { - throw new SocketException(__d('cake_dev', 'SMTP timeout.')); - } - $responseLines = explode("\r\n", rtrim($response, "\r\n")); - $response = end($responseLines); + /** + * Prepares the message body. + * + * @param CakeEmail $email CakeEmail + * @return string + */ + protected function _prepareMessage(CakeEmail $email) + { + $lines = $email->message(); + $messages = []; + foreach ($lines as $line) { + if ((!empty($line)) && ($line[0] === '.')) { + $messages[] = '.' . $line; + } else { + $messages[] = $line; + } + } + return implode("\r\n", $messages); + } - $this->_bufferResponseLines($responseLines); + /** + * Disconnect + * + * @return void + * @throws SocketException + */ + protected function _disconnect() + { + $this->_smtpSend('QUIT', false); + $this->_socket->disconnect(); + } - if (preg_match('/^(' . $checkCode . ')(.)/', $response, $code)) { - if ($code[2] === '-') { - continue; - } - return $code[1]; - } - throw new SocketException(__d('cake_dev', 'SMTP Error: %s', $response)); - } - } + /** + * Set the configuration + * + * @param array $config Configuration options. + * @return array Returns configs + */ + public function config($config = null) + { + if ($config === null) { + return $this->_config; + } + $default = [ + 'host' => 'localhost', + 'port' => 25, + 'timeout' => 30, + 'username' => null, + 'password' => null, + 'client' => null, + 'tls' => false, + 'ssl_allow_self_signed' => false + ]; + $this->_config = array_merge($default, $this->_config, $config); + return $this->_config; + } } diff --git a/lib/Cake/Network/Http/BasicAuthentication.php b/lib/Cake/Network/Http/BasicAuthentication.php index 5d252028..06a2ffb4 100755 --- a/lib/Cake/Network/Http/BasicAuthentication.php +++ b/lib/Cake/Network/Http/BasicAuthentication.php @@ -21,45 +21,49 @@ * * @package Cake.Network.Http */ -class BasicAuthentication { +class BasicAuthentication +{ -/** - * Authentication - * - * @param HttpSocket $http Http socket instance. - * @param array &$authInfo Authentication info. - * @return void - * @see http://www.ietf.org/rfc/rfc2617.txt - */ - public static function authentication(HttpSocket $http, &$authInfo) { - if (isset($authInfo['user'], $authInfo['pass'])) { - $http->request['header']['Authorization'] = static::_generateHeader($authInfo['user'], $authInfo['pass']); - } - } + /** + * Authentication + * + * @param HttpSocket $http Http socket instance. + * @param array &$authInfo Authentication info. + * @return void + * @see http://www.ietf.org/rfc/rfc2617.txt + */ + public static function authentication(HttpSocket $http, &$authInfo) + { + if (isset($authInfo['user'], $authInfo['pass'])) { + $http->request['header']['Authorization'] = static::_generateHeader($authInfo['user'], $authInfo['pass']); + } + } -/** - * Proxy Authentication - * - * @param HttpSocket $http Http socket instance. - * @param array &$proxyInfo Proxy info. - * @return void - * @see http://www.ietf.org/rfc/rfc2617.txt - */ - public static function proxyAuthentication(HttpSocket $http, &$proxyInfo) { - if (isset($proxyInfo['user'], $proxyInfo['pass'])) { - $http->request['header']['Proxy-Authorization'] = static::_generateHeader($proxyInfo['user'], $proxyInfo['pass']); - } - } + /** + * Generate basic [proxy] authentication header + * + * @param string $user Username. + * @param string $pass Password. + * @return string + */ + protected static function _generateHeader($user, $pass) + { + return 'Basic ' . base64_encode($user . ':' . $pass); + } -/** - * Generate basic [proxy] authentication header - * - * @param string $user Username. - * @param string $pass Password. - * @return string - */ - protected static function _generateHeader($user, $pass) { - return 'Basic ' . base64_encode($user . ':' . $pass); - } + /** + * Proxy Authentication + * + * @param HttpSocket $http Http socket instance. + * @param array &$proxyInfo Proxy info. + * @return void + * @see http://www.ietf.org/rfc/rfc2617.txt + */ + public static function proxyAuthentication(HttpSocket $http, &$proxyInfo) + { + if (isset($proxyInfo['user'], $proxyInfo['pass'])) { + $http->request['header']['Proxy-Authorization'] = static::_generateHeader($proxyInfo['user'], $proxyInfo['pass']); + } + } } diff --git a/lib/Cake/Network/Http/DigestAuthentication.php b/lib/Cake/Network/Http/DigestAuthentication.php index 65dd5a78..2e619252 100755 --- a/lib/Cake/Network/Http/DigestAuthentication.php +++ b/lib/Cake/Network/Http/DigestAuthentication.php @@ -21,84 +21,88 @@ * * @package Cake.Network.Http */ -class DigestAuthentication { +class DigestAuthentication +{ -/** - * Authentication - * - * @param HttpSocket $http Http socket instance. - * @param array &$authInfo Authentication info. - * @return void - * @link http://www.ietf.org/rfc/rfc2617.txt - */ - public static function authentication(HttpSocket $http, &$authInfo) { - if (isset($authInfo['user'], $authInfo['pass'])) { - if (!isset($authInfo['realm']) && !static::_getServerInformation($http, $authInfo)) { - return; - } - $http->request['header']['Authorization'] = static::_generateHeader($http, $authInfo); - } - } + /** + * Authentication + * + * @param HttpSocket $http Http socket instance. + * @param array &$authInfo Authentication info. + * @return void + * @link http://www.ietf.org/rfc/rfc2617.txt + */ + public static function authentication(HttpSocket $http, &$authInfo) + { + if (isset($authInfo['user'], $authInfo['pass'])) { + if (!isset($authInfo['realm']) && !static::_getServerInformation($http, $authInfo)) { + return; + } + $http->request['header']['Authorization'] = static::_generateHeader($http, $authInfo); + } + } -/** - * Retrieve information about the authentication - * - * @param HttpSocket $http Http socket instance. - * @param array &$authInfo Authentication info. - * @return bool - */ - protected static function _getServerInformation(HttpSocket $http, &$authInfo) { - $originalRequest = $http->request; - $http->configAuth(false); - $http->request($http->request); - $http->request = $originalRequest; - $http->configAuth('Digest', $authInfo); + /** + * Retrieve information about the authentication + * + * @param HttpSocket $http Http socket instance. + * @param array &$authInfo Authentication info. + * @return bool + */ + protected static function _getServerInformation(HttpSocket $http, &$authInfo) + { + $originalRequest = $http->request; + $http->configAuth(false); + $http->request($http->request); + $http->request = $originalRequest; + $http->configAuth('Digest', $authInfo); - if (empty($http->response['header']['WWW-Authenticate'])) { - return false; - } - preg_match_all('@(\w+)=(?:(?:")([^"]+)"|([^\s,$]+))@', $http->response['header']['WWW-Authenticate'], $matches, PREG_SET_ORDER); - foreach ($matches as $match) { - $authInfo[$match[1]] = $match[2]; - } - if (!empty($authInfo['qop']) && empty($authInfo['nc'])) { - $authInfo['nc'] = 1; - } - return true; - } + if (empty($http->response['header']['WWW-Authenticate'])) { + return false; + } + preg_match_all('@(\w+)=(?:(?:")([^"]+)"|([^\s,$]+))@', $http->response['header']['WWW-Authenticate'], $matches, PREG_SET_ORDER); + foreach ($matches as $match) { + $authInfo[$match[1]] = $match[2]; + } + if (!empty($authInfo['qop']) && empty($authInfo['nc'])) { + $authInfo['nc'] = 1; + } + return true; + } -/** - * Generate the header Authorization - * - * @param HttpSocket $http Http socket instance. - * @param array &$authInfo Authentication info. - * @return string - */ - protected static function _generateHeader(HttpSocket $http, &$authInfo) { - $a1 = md5($authInfo['user'] . ':' . $authInfo['realm'] . ':' . $authInfo['pass']); - $a2 = md5($http->request['method'] . ':' . $http->request['uri']['path']); + /** + * Generate the header Authorization + * + * @param HttpSocket $http Http socket instance. + * @param array &$authInfo Authentication info. + * @return string + */ + protected static function _generateHeader(HttpSocket $http, &$authInfo) + { + $a1 = md5($authInfo['user'] . ':' . $authInfo['realm'] . ':' . $authInfo['pass']); + $a2 = md5($http->request['method'] . ':' . $http->request['uri']['path']); - if (empty($authInfo['qop'])) { - $response = md5($a1 . ':' . $authInfo['nonce'] . ':' . $a2); - } else { - $authInfo['cnonce'] = uniqid(); - $nc = sprintf('%08x', $authInfo['nc']++); - $response = md5($a1 . ':' . $authInfo['nonce'] . ':' . $nc . ':' . $authInfo['cnonce'] . ':auth:' . $a2); - } + if (empty($authInfo['qop'])) { + $response = md5($a1 . ':' . $authInfo['nonce'] . ':' . $a2); + } else { + $authInfo['cnonce'] = uniqid(); + $nc = sprintf('%08x', $authInfo['nc']++); + $response = md5($a1 . ':' . $authInfo['nonce'] . ':' . $nc . ':' . $authInfo['cnonce'] . ':auth:' . $a2); + } - $authHeader = 'Digest '; - $authHeader .= 'username="' . str_replace(array('\\', '"'), array('\\\\', '\\"'), $authInfo['user']) . '", '; - $authHeader .= 'realm="' . $authInfo['realm'] . '", '; - $authHeader .= 'nonce="' . $authInfo['nonce'] . '", '; - $authHeader .= 'uri="' . $http->request['uri']['path'] . '", '; - $authHeader .= 'response="' . $response . '"'; - if (!empty($authInfo['opaque'])) { - $authHeader .= ', opaque="' . $authInfo['opaque'] . '"'; - } - if (!empty($authInfo['qop'])) { - $authHeader .= ', qop="auth", nc=' . $nc . ', cnonce="' . $authInfo['cnonce'] . '"'; - } - return $authHeader; - } + $authHeader = 'Digest '; + $authHeader .= 'username="' . str_replace(['\\', '"'], ['\\\\', '\\"'], $authInfo['user']) . '", '; + $authHeader .= 'realm="' . $authInfo['realm'] . '", '; + $authHeader .= 'nonce="' . $authInfo['nonce'] . '", '; + $authHeader .= 'uri="' . $http->request['uri']['path'] . '", '; + $authHeader .= 'response="' . $response . '"'; + if (!empty($authInfo['opaque'])) { + $authHeader .= ', opaque="' . $authInfo['opaque'] . '"'; + } + if (!empty($authInfo['qop'])) { + $authHeader .= ', qop="auth", nc=' . $nc . ', cnonce="' . $authInfo['cnonce'] . '"'; + } + return $authHeader; + } } diff --git a/lib/Cake/Network/Http/HttpResponse.php b/lib/Cake/Network/Http/HttpResponse.php index 61ffab0b..76a5f09a 100755 --- a/lib/Cake/Network/Http/HttpResponse.php +++ b/lib/Cake/Network/Http/HttpResponse.php @@ -18,10 +18,10 @@ App::uses('HttpSocketResponse', 'Network/Http'); if (class_exists('HttpResponse')) { - trigger_error(__d( - 'cake_dev', - "HttpResponse is deprecated due to naming conflicts. Use HttpSocketResponse instead." - ), E_USER_ERROR); + trigger_error(__d( + 'cake_dev', + "HttpResponse is deprecated due to naming conflicts. Use HttpSocketResponse instead." + ), E_USER_ERROR); } /** @@ -30,6 +30,7 @@ * @package Cake.Network.Http * @deprecated 3.0.0 This class is deprecated as it has naming conflicts with pecl/http */ -class HttpResponse extends HttpSocketResponse { +class HttpResponse extends HttpSocketResponse +{ } diff --git a/lib/Cake/Network/Http/HttpSocket.php b/lib/Cake/Network/Http/HttpSocket.php index 2e62bada..73f694a0 100755 --- a/lib/Cake/Network/Http/HttpSocket.php +++ b/lib/Cake/Network/Http/HttpSocket.php @@ -28,1016 +28,1041 @@ * * @package Cake.Network.Http */ -class HttpSocket extends CakeSocket { - -/** - * When one activates the $quirksMode by setting it to true, all checks meant to - * enforce RFC 2616 (HTTP/1.1 specs). - * will be disabled and additional measures to deal with non-standard responses will be enabled. - * - * @var bool - */ - public $quirksMode = false; - -/** - * Contain information about the last request (read only) - * - * @var array - */ - public $request = array( - 'method' => 'GET', - 'uri' => array( - 'scheme' => 'http', - 'host' => null, - 'port' => 80, - 'user' => null, - 'pass' => null, - 'path' => null, - 'query' => null, - 'fragment' => null - ), - 'version' => '1.1', - 'body' => '', - 'line' => null, - 'header' => array( - 'Connection' => 'close', - 'User-Agent' => 'CakePHP' - ), - 'raw' => null, - 'redirect' => false, - 'cookies' => array(), - ); - -/** - * Contain information about the last response (read only) - * - * @var array - */ - public $response = null; - -/** - * Response class name - * - * @var string - */ - public $responseClass = 'HttpSocketResponse'; - -/** - * Configuration settings for the HttpSocket and the requests - * - * @var array - */ - public $config = array( - 'persistent' => false, - 'host' => 'localhost', - 'protocol' => 'tcp', - 'port' => 80, - 'timeout' => 30, - 'ssl_verify_peer' => true, - 'ssl_allow_self_signed' => false, - 'ssl_verify_depth' => 5, - 'ssl_verify_host' => true, - 'request' => array( - 'uri' => array( - 'scheme' => array('http', 'https'), - 'host' => 'localhost', - 'port' => array(80, 443) - ), - 'redirect' => false, - 'cookies' => array(), - ) - ); - -/** - * Authentication settings - * - * @var array - */ - protected $_auth = array(); - -/** - * Proxy settings - * - * @var array - */ - protected $_proxy = array(); - -/** - * Resource to receive the content of request - * - * @var mixed - */ - protected $_contentResource = null; - -/** - * Build an HTTP Socket using the specified configuration. - * - * You can use a URL string to set the URL and use default configurations for - * all other options: - * - * `$http = new HttpSocket('https://cakephp.org/');` - * - * Or use an array to configure multiple options: - * - * ``` - * $http = new HttpSocket(array( - * 'host' => 'cakephp.org', - * 'timeout' => 20 - * )); - * ``` - * - * See HttpSocket::$config for options that can be used. - * - * @param string|array $config Configuration information, either a string URL or an array of options. - */ - public function __construct($config = array()) { - if (is_string($config)) { - $this->_configUri($config); - } elseif (is_array($config)) { - if (isset($config['request']['uri']) && is_string($config['request']['uri'])) { - $this->_configUri($config['request']['uri']); - unset($config['request']['uri']); - } - $this->config = Hash::merge($this->config, $config); - } - parent::__construct($this->config); - } - -/** - * Set authentication settings. - * - * Accepts two forms of parameters. If all you need is a username + password, as with - * Basic authentication you can do the following: - * - * ``` - * $http->configAuth('Basic', 'mark', 'secret'); - * ``` - * - * If you are using an authentication strategy that requires more inputs, like Digest authentication - * you can call `configAuth()` with an array of user information. - * - * ``` - * $http->configAuth('Digest', array( - * 'user' => 'mark', - * 'pass' => 'secret', - * 'realm' => 'my-realm', - * 'nonce' => 1235 - * )); - * ``` - * - * To remove any set authentication strategy, call `configAuth()` with no parameters: - * - * `$http->configAuth();` - * - * @param string $method Authentication method (ie. Basic, Digest). If empty, disable authentication - * @param string|array $user Username for authentication. Can be an array with settings to authentication class - * @param string $pass Password for authentication - * @return void - */ - public function configAuth($method, $user = null, $pass = null) { - if (empty($method)) { - $this->_auth = array(); - return; - } - if (is_array($user)) { - $this->_auth = array($method => $user); - return; - } - $this->_auth = array($method => compact('user', 'pass')); - } - -/** - * Set proxy settings - * - * @param string|array $host Proxy host. Can be an array with settings to authentication class - * @param int $port Port. Default 3128. - * @param string $method Proxy method (ie, Basic, Digest). If empty, disable proxy authentication - * @param string $user Username if your proxy need authentication - * @param string $pass Password to proxy authentication - * @return void - */ - public function configProxy($host, $port = 3128, $method = null, $user = null, $pass = null) { - if (empty($host)) { - $this->_proxy = array(); - return; - } - if (is_array($host)) { - $this->_proxy = $host + array('host' => null); - return; - } - $this->_proxy = compact('host', 'port', 'method', 'user', 'pass'); - } - -/** - * Set the resource to receive the request content. This resource must support fwrite. - * - * @param resource|bool $resource Resource or false to disable the resource use - * @return void - * @throws SocketException - */ - public function setContentResource($resource) { - if ($resource === false) { - $this->_contentResource = null; - return; - } - if (!is_resource($resource)) { - throw new SocketException(__d('cake_dev', 'Invalid resource.')); - } - $this->_contentResource = $resource; - } - -/** - * Issue the specified request. HttpSocket::get() and HttpSocket::post() wrap this - * method and provide a more granular interface. - * - * @param string|array $request Either an URI string, or an array defining host/uri - * @return false|HttpSocketResponse false on error, HttpSocketResponse on success - * @throws SocketException - */ - public function request($request = array()) { - $this->reset(false); - - if (is_string($request)) { - $request = array('uri' => $request); - } elseif (!is_array($request)) { - return false; - } - - if (!isset($request['uri'])) { - $request['uri'] = null; - } - $uri = $this->_parseUri($request['uri']); - if (!isset($uri['host'])) { - $host = $this->config['host']; - } - if (isset($request['host'])) { - $host = $request['host']; - unset($request['host']); - } - $request['uri'] = $this->url($request['uri']); - $request['uri'] = $this->_parseUri($request['uri'], true); - $this->request = Hash::merge($this->request, array_diff_key($this->config['request'], array('cookies' => true)), $request); - - $this->_configUri($this->request['uri']); - - $Host = $this->request['uri']['host']; - if (!empty($this->config['request']['cookies'][$Host])) { - if (!isset($this->request['cookies'])) { - $this->request['cookies'] = array(); - } - if (!isset($request['cookies'])) { - $request['cookies'] = array(); - } - $this->request['cookies'] = array_merge($this->request['cookies'], $this->config['request']['cookies'][$Host], $request['cookies']); - } - - if (isset($host)) { - $this->config['host'] = $host; - } - - $this->_setProxy(); - $this->request['proxy'] = $this->_proxy; - - $cookies = null; - - if (is_array($this->request['header'])) { - if (!empty($this->request['cookies'])) { - $cookies = $this->buildCookies($this->request['cookies']); - } - $scheme = ''; - $port = 0; - if (isset($this->request['uri']['scheme'])) { - $scheme = $this->request['uri']['scheme']; - } - if (isset($this->request['uri']['port'])) { - $port = $this->request['uri']['port']; - } - if (($scheme === 'http' && $port != 80) || - ($scheme === 'https' && $port != 443) || - ($port != 80 && $port != 443) - ) { - $Host .= ':' . $port; - } - $this->request['header'] = array_merge(compact('Host'), $this->request['header']); - } - - if (isset($this->request['uri']['user'], $this->request['uri']['pass'])) { - $this->configAuth('Basic', $this->request['uri']['user'], $this->request['uri']['pass']); - } elseif (isset($this->request['auth'], $this->request['auth']['method'], $this->request['auth']['user'], $this->request['auth']['pass'])) { - $this->configAuth($this->request['auth']['method'], $this->request['auth']['user'], $this->request['auth']['pass']); - } - $authHeader = Hash::get($this->request, 'header.Authorization'); - if (empty($authHeader)) { - $this->_setAuth(); - $this->request['auth'] = $this->_auth; - } - - if (is_array($this->request['body'])) { - $this->request['body'] = http_build_query($this->request['body'], '', '&'); - } - - if (!empty($this->request['body']) && !isset($this->request['header']['Content-Type'])) { - $this->request['header']['Content-Type'] = 'application/x-www-form-urlencoded'; - } - - if (!empty($this->request['body']) && !isset($this->request['header']['Content-Length'])) { - $this->request['header']['Content-Length'] = strlen($this->request['body']); - } - if (isset($this->request['uri']['scheme']) && $this->request['uri']['scheme'] === 'https' && in_array($this->config['protocol'], array(false, 'tcp'))) { - $this->config['protocol'] = 'ssl'; - } - - $connectionType = null; - if (isset($this->request['header']['Connection'])) { - $connectionType = $this->request['header']['Connection']; - } - $this->request['header'] = $this->_buildHeader($this->request['header']) . $cookies; - - if (empty($this->request['line'])) { - $this->request['line'] = $this->_buildRequestLine($this->request); - } - - if ($this->quirksMode === false && $this->request['line'] === false) { - return false; - } - - $this->request['raw'] = ''; - if ($this->request['line'] !== false) { - $this->request['raw'] = $this->request['line']; - } - - if ($this->request['header'] !== false) { - $this->request['raw'] .= $this->request['header']; - } - - $this->request['raw'] .= "\r\n"; - $this->request['raw'] .= $this->request['body']; - - // SSL context is set during the connect() method. - $this->write($this->request['raw']); - - $response = null; - $inHeader = true; - while (($data = $this->read()) !== false) { - if ($this->_contentResource) { - if ($inHeader) { - $response .= $data; - $pos = strpos($response, "\r\n\r\n"); - if ($pos !== false) { - $pos += 4; - $data = substr($response, $pos); - fwrite($this->_contentResource, $data); - - $response = substr($response, 0, $pos); - $inHeader = false; - } - } else { - fwrite($this->_contentResource, $data); - fflush($this->_contentResource); - } - } else { - $response .= $data; - } - } - - if ($connectionType === 'close') { - $this->disconnect(); - } - - list($plugin, $responseClass) = pluginSplit($this->responseClass, true); - App::uses($responseClass, $plugin . 'Network/Http'); - if (!class_exists($responseClass)) { - throw new SocketException(__d('cake_dev', 'Class %s not found.', $this->responseClass)); - } - $this->response = new $responseClass($response); - - if (!empty($this->response->cookies)) { - if (!isset($this->config['request']['cookies'][$Host])) { - $this->config['request']['cookies'][$Host] = array(); - } - $this->config['request']['cookies'][$Host] = array_merge($this->config['request']['cookies'][$Host], $this->response->cookies); - } - - if ($this->request['redirect'] && $this->response->isRedirect()) { - $location = trim($this->response->getHeader('Location'), '='); - $request['uri'] = str_replace('%2F', '/', $location); - $request['redirect'] = is_int($this->request['redirect']) ? $this->request['redirect'] - 1 : $this->request['redirect']; - $this->response = $this->request($request); - } - - return $this->response; - } - -/** - * Issues a GET request to the specified URI, query, and request. - * - * Using a string uri and an array of query string parameters: - * - * `$response = $http->get('http://google.com/search', array('q' => 'cakephp', 'client' => 'safari'));` - * - * Would do a GET request to `http://google.com/search?q=cakephp&client=safari` - * - * You could express the same thing using a uri array and query string parameters: - * - * ``` - * $response = $http->get( - * array('host' => 'google.com', 'path' => '/search'), - * array('q' => 'cakephp', 'client' => 'safari') - * ); - * ``` - * - * @param string|array $uri URI to request. Either a string uri, or a uri array, see HttpSocket::_parseUri() - * @param array $query Querystring parameters to append to URI - * @param array $request An indexed array with indexes such as 'method' or uri - * @return false|HttpSocketResponse Result of request, either false on failure or the response to the request. - */ - public function get($uri = null, $query = array(), $request = array()) { - $uri = $this->_parseUri($uri, $this->config['request']['uri']); - if (isset($uri['query'])) { - $uri['query'] = array_merge($uri['query'], $query); - } else { - $uri['query'] = $query; - } - $uri = $this->_buildUri($uri); - - $request = Hash::merge(array('method' => 'GET', 'uri' => $uri), $request); - return $this->request($request); - } - -/** - * Issues a HEAD request to the specified URI, query, and request. - * - * By definition HEAD request are identical to GET request except they return no response body. This means that all - * information and examples relevant to GET also applys to HEAD. - * - * @param string|array $uri URI to request. Either a string URI, or a URI array, see HttpSocket::_parseUri() - * @param array $query Querystring parameters to append to URI - * @param array $request An indexed array with indexes such as 'method' or uri - * @return false|HttpSocketResponse Result of request, either false on failure or the response to the request. - */ - public function head($uri = null, $query = array(), $request = array()) { - $uri = $this->_parseUri($uri, $this->config['request']['uri']); - if (isset($uri['query'])) { - $uri['query'] = array_merge($uri['query'], $query); - } else { - $uri['query'] = $query; - } - $uri = $this->_buildUri($uri); - - $request = Hash::merge(array('method' => 'HEAD', 'uri' => $uri), $request); - return $this->request($request); - } - -/** - * Issues a POST request to the specified URI, query, and request. - * - * `post()` can be used to post simple data arrays to a URL: - * - * ``` - * $response = $http->post('http://example.com', array( - * 'username' => 'batman', - * 'password' => 'bruce_w4yne' - * )); - * ``` - * - * @param string|array $uri URI to request. See HttpSocket::_parseUri() - * @param array $data Array of request body data keys and values. - * @param array $request An indexed array with indexes such as 'method' or uri - * @return false|HttpSocketResponse Result of request, either false on failure or the response to the request. - */ - public function post($uri = null, $data = array(), $request = array()) { - $request = Hash::merge(array('method' => 'POST', 'uri' => $uri, 'body' => $data), $request); - return $this->request($request); - } - -/** - * Issues a PUT request to the specified URI, query, and request. - * - * @param string|array $uri URI to request, See HttpSocket::_parseUri() - * @param array $data Array of request body data keys and values. - * @param array $request An indexed array with indexes such as 'method' or uri - * @return false|HttpSocketResponse Result of request - */ - public function put($uri = null, $data = array(), $request = array()) { - $request = Hash::merge(array('method' => 'PUT', 'uri' => $uri, 'body' => $data), $request); - return $this->request($request); - } - -/** - * Issues a PATCH request to the specified URI, query, and request. - * - * @param string|array $uri URI to request, See HttpSocket::_parseUri() - * @param array $data Array of request body data keys and values. - * @param array $request An indexed array with indexes such as 'method' or uri - * @return false|HttpSocketResponse Result of request - */ - public function patch($uri = null, $data = array(), $request = array()) { - $request = Hash::merge(array('method' => 'PATCH', 'uri' => $uri, 'body' => $data), $request); - return $this->request($request); - } - -/** - * Issues a DELETE request to the specified URI, query, and request. - * - * @param string|array $uri URI to request (see {@link _parseUri()}) - * @param array $data Array of request body data keys and values. - * @param array $request An indexed array with indexes such as 'method' or uri - * @return false|HttpSocketResponse Result of request - */ - public function delete($uri = null, $data = array(), $request = array()) { - $request = Hash::merge(array('method' => 'DELETE', 'uri' => $uri, 'body' => $data), $request); - return $this->request($request); - } - -/** - * Normalizes URLs into a $uriTemplate. If no template is provided - * a default one will be used. Will generate the URL using the - * current config information. - * - * ### Usage: - * - * After configuring part of the request parameters, you can use url() to generate - * URLs. - * - * ``` - * $http = new HttpSocket('https://www.cakephp.org'); - * $url = $http->url('/search?q=bar'); - * ``` - * - * Would return `https://cakephp.org/search?q=bar` - * - * url() can also be used with custom templates: - * - * `$url = $http->url('http://www.cakephp/search?q=socket', '/%path?%query');` - * - * Would return `/search?q=socket`. - * - * @param string|array $url Either a string or array of URL options to create a URL with. - * @param string $uriTemplate A template string to use for URL formatting. - * @return mixed Either false on failure or a string containing the composed URL. - */ - public function url($url = null, $uriTemplate = null) { - if ($url === null) { - $url = '/'; - } - if (is_string($url)) { - $scheme = $this->config['request']['uri']['scheme']; - if (is_array($scheme)) { - $scheme = $scheme[0]; - } - $port = $this->config['request']['uri']['port']; - if (is_array($port)) { - $port = $port[0]; - } - if ($url{0} === '/') { - $url = $this->config['request']['uri']['host'] . ':' . $port . $url; - } - if (!preg_match('/^.+:\/\/|\*|^\//', $url)) { - $url = $scheme . '://' . $url; - } - } elseif (!is_array($url) && !empty($url)) { - return false; - } - - $base = array_merge($this->config['request']['uri'], array('scheme' => array('http', 'https'), 'port' => array(80, 443))); - $url = $this->_parseUri($url, $base); - - if (empty($url)) { - $url = $this->config['request']['uri']; - } - - if (!empty($uriTemplate)) { - return $this->_buildUri($url, $uriTemplate); - } - return $this->_buildUri($url); - } - -/** - * Set authentication in request - * - * @return void - * @throws SocketException - */ - protected function _setAuth() { - if (empty($this->_auth)) { - return; - } - $method = key($this->_auth); - list($plugin, $authClass) = pluginSplit($method, true); - $authClass = Inflector::camelize($authClass) . 'Authentication'; - App::uses($authClass, $plugin . 'Network/Http'); - - if (!class_exists($authClass)) { - throw new SocketException(__d('cake_dev', 'Unknown authentication method.')); - } - if (!method_exists($authClass, 'authentication')) { - throw new SocketException(__d('cake_dev', 'The %s does not support authentication.', $authClass)); - } - call_user_func_array("$authClass::authentication", array($this, &$this->_auth[$method])); - } - -/** - * Set the proxy configuration and authentication - * - * @return void - * @throws SocketException - */ - protected function _setProxy() { - if (empty($this->_proxy) || !isset($this->_proxy['host'], $this->_proxy['port'])) { - return; - } - $this->config['host'] = $this->_proxy['host']; - $this->config['port'] = $this->_proxy['port']; - $this->config['proxy'] = true; - - if (empty($this->_proxy['method']) || !isset($this->_proxy['user'], $this->_proxy['pass'])) { - return; - } - list($plugin, $authClass) = pluginSplit($this->_proxy['method'], true); - $authClass = Inflector::camelize($authClass) . 'Authentication'; - App::uses($authClass, $plugin . 'Network/Http'); - - if (!class_exists($authClass)) { - throw new SocketException(__d('cake_dev', 'Unknown authentication method for proxy.')); - } - if (!method_exists($authClass, 'proxyAuthentication')) { - throw new SocketException(__d('cake_dev', 'The %s does not support proxy authentication.', $authClass)); - } - call_user_func_array("$authClass::proxyAuthentication", array($this, &$this->_proxy)); - - if (!empty($this->request['header']['Proxy-Authorization'])) { - $this->config['proxyauth'] = $this->request['header']['Proxy-Authorization']; - if ($this->request['uri']['scheme'] === 'https') { - $this->request['header'] = Hash::remove($this->request['header'], 'Proxy-Authorization'); - } - } - } - -/** - * Parses and sets the specified URI into current request configuration. - * - * @param string|array $uri URI, See HttpSocket::_parseUri() - * @return bool If uri has merged in config - */ - protected function _configUri($uri = null) { - if (empty($uri)) { - return false; - } - - if (is_array($uri)) { - $uri = $this->_parseUri($uri); - } else { - $uri = $this->_parseUri($uri, true); - } - - if (!isset($uri['host'])) { - return false; - } - $config = array( - 'request' => array( - 'uri' => array_intersect_key($uri, $this->config['request']['uri']) - ) - ); - $this->config = Hash::merge($this->config, $config); - $this->config = Hash::merge($this->config, array_intersect_key($this->config['request']['uri'], $this->config)); - return true; - } - -/** - * Takes a $uri array and turns it into a fully qualified URL string - * - * @param string|array $uri Either A $uri array, or a request string. Will use $this->config if left empty. - * @param string $uriTemplate The Uri template/format to use. - * @return mixed A fully qualified URL formatted according to $uriTemplate, or false on failure - */ - protected function _buildUri($uri = array(), $uriTemplate = '%scheme://%user:%pass@%host:%port/%path?%query#%fragment') { - if (is_string($uri)) { - $uri = array('host' => $uri); - } - $uri = $this->_parseUri($uri, true); - - if (!is_array($uri) || empty($uri)) { - return false; - } - - $uri['path'] = preg_replace('/^\//', null, $uri['path']); - $uri['query'] = http_build_query($uri['query'], '', '&'); - $uri['query'] = rtrim($uri['query'], '='); - $stripIfEmpty = array( - 'query' => '?%query', - 'fragment' => '#%fragment', - 'user' => '%user:%pass@', - 'host' => '%host:%port/' - ); - - foreach ($stripIfEmpty as $key => $strip) { - if (empty($uri[$key])) { - $uriTemplate = str_replace($strip, null, $uriTemplate); - } - } - - $defaultPorts = array('http' => 80, 'https' => 443); - if (array_key_exists($uri['scheme'], $defaultPorts) && $defaultPorts[$uri['scheme']] == $uri['port']) { - $uriTemplate = str_replace(':%port', null, $uriTemplate); - } - foreach ($uri as $property => $value) { - $uriTemplate = str_replace('%' . $property, $value, $uriTemplate); - } - - if ($uriTemplate === '/*') { - $uriTemplate = '*'; - } - return $uriTemplate; - } - -/** - * Parses the given URI and breaks it down into pieces as an indexed array with elements - * such as 'scheme', 'port', 'query'. - * - * @param string|array $uri URI to parse - * @param bool|array $base If true use default URI config, otherwise indexed array to set 'scheme', 'host', 'port', etc. - * @return array|bool Parsed URI - */ - protected function _parseUri($uri = null, $base = array()) { - $uriBase = array( - 'scheme' => array('http', 'https'), - 'host' => null, - 'port' => array(80, 443), - 'user' => null, - 'pass' => null, - 'path' => '/', - 'query' => null, - 'fragment' => null - ); - - if (is_string($uri)) { - $uri = parse_url($uri); - } - if (!is_array($uri) || empty($uri)) { - return false; - } - if ($base === true) { - $base = $uriBase; - } - - if (isset($base['port'], $base['scheme']) && is_array($base['port']) && is_array($base['scheme'])) { - if (isset($uri['scheme']) && !isset($uri['port'])) { - $base['port'] = $base['port'][array_search($uri['scheme'], $base['scheme'])]; - } elseif (isset($uri['port']) && !isset($uri['scheme'])) { - $base['scheme'] = $base['scheme'][array_search($uri['port'], $base['port'])]; - } - } - - if (is_array($base) && !empty($base)) { - $uri = array_merge($base, $uri); - } - - if (isset($uri['scheme']) && is_array($uri['scheme'])) { - $uri['scheme'] = array_shift($uri['scheme']); - } - if (isset($uri['port']) && is_array($uri['port'])) { - $uri['port'] = array_shift($uri['port']); - } - - if (array_key_exists('query', $uri)) { - $uri['query'] = $this->_parseQuery($uri['query']); - } - - if (!array_intersect_key($uriBase, $uri)) { - return false; - } - return $uri; - } - -/** - * This function can be thought of as a reverse to PHP5's http_build_query(). It takes a given query string and turns it into an array and - * supports nesting by using the php bracket syntax. So this means you can parse queries like: - * - * - ?key[subKey]=value - * - ?key[]=value1&key[]=value2 - * - * A leading '?' mark in $query is optional and does not effect the outcome of this function. - * For the complete capabilities of this implementation take a look at HttpSocketTest::testparseQuery() - * - * @param string|array $query A query string to parse into an array or an array to return directly "as is" - * @return array The $query parsed into a possibly multi-level array. If an empty $query is - * given, an empty array is returned. - */ - protected function _parseQuery($query) { - if (is_array($query)) { - return $query; - } - - $parsedQuery = array(); - - if (is_string($query) && !empty($query)) { - $query = preg_replace('/^\?/', '', $query); - $items = explode('&', $query); - - foreach ($items as $item) { - if (strpos($item, '=') !== false) { - list($key, $value) = explode('=', $item, 2); - } else { - $key = $item; - $value = null; - } - - $key = urldecode($key); - $value = urldecode($value); - - if (preg_match_all('/\[([^\[\]]*)\]/iUs', $key, $matches)) { - $subKeys = $matches[1]; - $rootKey = substr($key, 0, strpos($key, '[')); - if (!empty($rootKey)) { - array_unshift($subKeys, $rootKey); - } - $queryNode =& $parsedQuery; - - foreach ($subKeys as $subKey) { - if (!is_array($queryNode)) { - $queryNode = array(); - } - - if ($subKey === '') { - $queryNode[] = array(); - end($queryNode); - $subKey = key($queryNode); - } - $queryNode =& $queryNode[$subKey]; - } - $queryNode = $value; - continue; - } - if (!isset($parsedQuery[$key])) { - $parsedQuery[$key] = $value; - } else { - $parsedQuery[$key] = (array)$parsedQuery[$key]; - $parsedQuery[$key][] = $value; - } - } - } - return $parsedQuery; - } - -/** - * Builds a request line according to HTTP/1.1 specs. Activate quirks mode to work outside specs. - * - * @param array $request Needs to contain a 'uri' key. Should also contain a 'method' key, otherwise defaults to GET. - * @return string Request line - * @throws SocketException - */ - protected function _buildRequestLine($request = array()) { - $asteriskMethods = array('OPTIONS'); - - if (is_string($request)) { - $isValid = preg_match("/(.+) (.+) (.+)\r\n/U", $request, $match); - if (!$this->quirksMode && (!$isValid || ($match[2] === '*' && !in_array($match[3], $asteriskMethods)))) { - throw new SocketException(__d('cake_dev', 'HttpSocket::_buildRequestLine - Passed an invalid request line string. Activate quirks mode to do this.')); - } - return $request; - } elseif (!is_array($request)) { - return false; - } elseif (!array_key_exists('uri', $request)) { - return false; - } - - $request['uri'] = $this->_parseUri($request['uri']); - $request += array('method' => 'GET'); - if (!empty($this->_proxy['host']) && $request['uri']['scheme'] !== 'https') { - $request['uri'] = $this->_buildUri($request['uri'], '%scheme://%host:%port/%path?%query'); - } else { - $request['uri'] = $this->_buildUri($request['uri'], '/%path?%query'); - } - - if (!$this->quirksMode && $request['uri'] === '*' && !in_array($request['method'], $asteriskMethods)) { - throw new SocketException(__d('cake_dev', 'HttpSocket::_buildRequestLine - The "*" asterisk character is only allowed for the following methods: %s. Activate quirks mode to work outside of HTTP/1.1 specs.', implode(',', $asteriskMethods))); - } - $version = isset($request['version']) ? $request['version'] : '1.1'; - return $request['method'] . ' ' . $request['uri'] . ' HTTP/' . $version . "\r\n"; - } - -/** - * Builds the header. - * - * @param array $header Header to build - * @param string $mode Mode - * @return string Header built from array - */ - protected function _buildHeader($header, $mode = 'standard') { - if (is_string($header)) { - return $header; - } elseif (!is_array($header)) { - return false; - } - - $fieldsInHeader = array(); - foreach ($header as $key => $value) { - $lowKey = strtolower($key); - if (array_key_exists($lowKey, $fieldsInHeader)) { - $header[$fieldsInHeader[$lowKey]] = $value; - unset($header[$key]); - } else { - $fieldsInHeader[$lowKey] = $key; - } - } - - $returnHeader = ''; - foreach ($header as $field => $contents) { - if (is_array($contents) && $mode === 'standard') { - $contents = implode(',', $contents); - } - foreach ((array)$contents as $content) { - $contents = preg_replace("/\r\n(?![\t ])/", "\r\n ", $content); - $field = $this->_escapeToken($field); - - $returnHeader .= $field . ': ' . $contents . "\r\n"; - } - } - return $returnHeader; - } - -/** - * Builds cookie headers for a request. - * - * Cookies can either be in the format returned in responses, or - * a simple key => value pair. - * - * @param array $cookies Array of cookies to send with the request. - * @return string Cookie header string to be sent with the request. - */ - public function buildCookies($cookies) { - $header = array(); - foreach ($cookies as $name => $cookie) { - if (is_array($cookie)) { - $value = $this->_escapeToken($cookie['value'], array(';')); - } else { - $value = $this->_escapeToken($cookie, array(';')); - } - $header[] = $name . '=' . $value; - } - return $this->_buildHeader(array('Cookie' => implode('; ', $header)), 'pragmatic'); - } - -/** - * Escapes a given $token according to RFC 2616 (HTTP 1.1 specs) - * - * @param string $token Token to escape - * @param array $chars Characters to escape - * @return string Escaped token - */ - protected function _escapeToken($token, $chars = null) { - $regex = '/([' . implode('', $this->_tokenEscapeChars(true, $chars)) . '])/'; - $token = preg_replace($regex, '"\\1"', $token); - return $token; - } - -/** - * Gets escape chars according to RFC 2616 (HTTP 1.1 specs). - * - * @param bool $hex true to get them as HEX values, false otherwise - * @param array $chars Characters to escape - * @return array Escape chars - */ - protected function _tokenEscapeChars($hex = true, $chars = null) { - if (!empty($chars)) { - $escape = $chars; - } else { - $escape = array('"', "(", ")", "<", ">", "@", ",", ";", ":", "\\", "/", "[", "]", "?", "=", "{", "}", " "); - for ($i = 0; $i <= 31; $i++) { - $escape[] = chr($i); - } - $escape[] = chr(127); - } - - if (!$hex) { - return $escape; - } - foreach ($escape as $key => $char) { - $escape[$key] = '\\x' . str_pad(dechex(ord($char)), 2, '0', STR_PAD_LEFT); - } - return $escape; - } - -/** - * Resets the state of this HttpSocket instance to it's initial state (before CakeObject::__construct got executed) or does - * the same thing partially for the request and the response property only. - * - * @param bool $full If set to false only HttpSocket::response and HttpSocket::request are reset - * @return bool True on success - */ - public function reset($full = true) { - static $initalState = array(); - if (empty($initalState)) { - $initalState = get_class_vars(__CLASS__); - } - if (!$full) { - $this->request = $initalState['request']; - $this->response = $initalState['response']; - return true; - } - parent::reset($initalState); - return true; - } +class HttpSocket extends CakeSocket +{ + + /** + * When one activates the $quirksMode by setting it to true, all checks meant to + * enforce RFC 2616 (HTTP/1.1 specs). + * will be disabled and additional measures to deal with non-standard responses will be enabled. + * + * @var bool + */ + public $quirksMode = false; + + /** + * Contain information about the last request (read only) + * + * @var array + */ + public $request = [ + 'method' => 'GET', + 'uri' => [ + 'scheme' => 'http', + 'host' => null, + 'port' => 80, + 'user' => null, + 'pass' => null, + 'path' => null, + 'query' => null, + 'fragment' => null + ], + 'version' => '1.1', + 'body' => '', + 'line' => null, + 'header' => [ + 'Connection' => 'close', + 'User-Agent' => 'CakePHP' + ], + 'raw' => null, + 'redirect' => false, + 'cookies' => [], + ]; + + /** + * Contain information about the last response (read only) + * + * @var array + */ + public $response = null; + + /** + * Response class name + * + * @var string + */ + public $responseClass = 'HttpSocketResponse'; + + /** + * Configuration settings for the HttpSocket and the requests + * + * @var array + */ + public $config = [ + 'persistent' => false, + 'host' => 'localhost', + 'protocol' => 'tcp', + 'port' => 80, + 'timeout' => 30, + 'ssl_verify_peer' => true, + 'ssl_allow_self_signed' => false, + 'ssl_verify_depth' => 5, + 'ssl_verify_host' => true, + 'request' => [ + 'uri' => [ + 'scheme' => ['http', 'https'], + 'host' => 'localhost', + 'port' => [80, 443] + ], + 'redirect' => false, + 'cookies' => [], + ] + ]; + + /** + * Authentication settings + * + * @var array + */ + protected $_auth = []; + + /** + * Proxy settings + * + * @var array + */ + protected $_proxy = []; + + /** + * Resource to receive the content of request + * + * @var mixed + */ + protected $_contentResource = null; + + /** + * Build an HTTP Socket using the specified configuration. + * + * You can use a URL string to set the URL and use default configurations for + * all other options: + * + * `$http = new HttpSocket('https://cakephp.org/');` + * + * Or use an array to configure multiple options: + * + * ``` + * $http = new HttpSocket(array( + * 'host' => 'cakephp.org', + * 'timeout' => 20 + * )); + * ``` + * + * See HttpSocket::$config for options that can be used. + * + * @param string|array $config Configuration information, either a string URL or an array of options. + */ + public function __construct($config = []) + { + if (is_string($config)) { + $this->_configUri($config); + } else if (is_array($config)) { + if (isset($config['request']['uri']) && is_string($config['request']['uri'])) { + $this->_configUri($config['request']['uri']); + unset($config['request']['uri']); + } + $this->config = Hash::merge($this->config, $config); + } + parent::__construct($this->config); + } + + /** + * Parses and sets the specified URI into current request configuration. + * + * @param string|array $uri URI, See HttpSocket::_parseUri() + * @return bool If uri has merged in config + */ + protected function _configUri($uri = null) + { + if (empty($uri)) { + return false; + } + + if (is_array($uri)) { + $uri = $this->_parseUri($uri); + } else { + $uri = $this->_parseUri($uri, true); + } + + if (!isset($uri['host'])) { + return false; + } + $config = [ + 'request' => [ + 'uri' => array_intersect_key($uri, $this->config['request']['uri']) + ] + ]; + $this->config = Hash::merge($this->config, $config); + $this->config = Hash::merge($this->config, array_intersect_key($this->config['request']['uri'], $this->config)); + return true; + } + + /** + * Parses the given URI and breaks it down into pieces as an indexed array with elements + * such as 'scheme', 'port', 'query'. + * + * @param string|array $uri URI to parse + * @param bool|array $base If true use default URI config, otherwise indexed array to set 'scheme', 'host', 'port', etc. + * @return array|bool Parsed URI + */ + protected function _parseUri($uri = null, $base = []) + { + $uriBase = [ + 'scheme' => ['http', 'https'], + 'host' => null, + 'port' => [80, 443], + 'user' => null, + 'pass' => null, + 'path' => '/', + 'query' => null, + 'fragment' => null + ]; + + if (is_string($uri)) { + $uri = parse_url($uri); + } + if (!is_array($uri) || empty($uri)) { + return false; + } + if ($base === true) { + $base = $uriBase; + } + + if (isset($base['port'], $base['scheme']) && is_array($base['port']) && is_array($base['scheme'])) { + if (isset($uri['scheme']) && !isset($uri['port'])) { + $base['port'] = $base['port'][array_search($uri['scheme'], $base['scheme'])]; + } else if (isset($uri['port']) && !isset($uri['scheme'])) { + $base['scheme'] = $base['scheme'][array_search($uri['port'], $base['port'])]; + } + } + + if (is_array($base) && !empty($base)) { + $uri = array_merge($base, $uri); + } + + if (isset($uri['scheme']) && is_array($uri['scheme'])) { + $uri['scheme'] = array_shift($uri['scheme']); + } + if (isset($uri['port']) && is_array($uri['port'])) { + $uri['port'] = array_shift($uri['port']); + } + + if (array_key_exists('query', $uri)) { + $uri['query'] = $this->_parseQuery($uri['query']); + } + + if (!array_intersect_key($uriBase, $uri)) { + return false; + } + return $uri; + } + + /** + * This function can be thought of as a reverse to PHP5's http_build_query(). It takes a given query string and turns it into an array and + * supports nesting by using the php bracket syntax. So this means you can parse queries like: + * + * - ?key[subKey]=value + * - ?key[]=value1&key[]=value2 + * + * A leading '?' mark in $query is optional and does not effect the outcome of this function. + * For the complete capabilities of this implementation take a look at HttpSocketTest::testparseQuery() + * + * @param string|array $query A query string to parse into an array or an array to return directly "as is" + * @return array The $query parsed into a possibly multi-level array. If an empty $query is + * given, an empty array is returned. + */ + protected function _parseQuery($query) + { + if (is_array($query)) { + return $query; + } + + $parsedQuery = []; + + if (is_string($query) && !empty($query)) { + $query = preg_replace('/^\?/', '', $query); + $items = explode('&', $query); + + foreach ($items as $item) { + if (strpos($item, '=') !== false) { + list($key, $value) = explode('=', $item, 2); + } else { + $key = $item; + $value = null; + } + + $key = urldecode($key); + $value = urldecode($value); + + if (preg_match_all('/\[([^\[\]]*)\]/iUs', $key, $matches)) { + $subKeys = $matches[1]; + $rootKey = substr($key, 0, strpos($key, '[')); + if (!empty($rootKey)) { + array_unshift($subKeys, $rootKey); + } + $queryNode =& $parsedQuery; + + foreach ($subKeys as $subKey) { + if (!is_array($queryNode)) { + $queryNode = []; + } + + if ($subKey === '') { + $queryNode[] = []; + end($queryNode); + $subKey = key($queryNode); + } + $queryNode =& $queryNode[$subKey]; + } + $queryNode = $value; + continue; + } + if (!isset($parsedQuery[$key])) { + $parsedQuery[$key] = $value; + } else { + $parsedQuery[$key] = (array)$parsedQuery[$key]; + $parsedQuery[$key][] = $value; + } + } + } + return $parsedQuery; + } + + /** + * Set proxy settings + * + * @param string|array $host Proxy host. Can be an array with settings to authentication class + * @param int $port Port. Default 3128. + * @param string $method Proxy method (ie, Basic, Digest). If empty, disable proxy authentication + * @param string $user Username if your proxy need authentication + * @param string $pass Password to proxy authentication + * @return void + */ + public function configProxy($host, $port = 3128, $method = null, $user = null, $pass = null) + { + if (empty($host)) { + $this->_proxy = []; + return; + } + if (is_array($host)) { + $this->_proxy = $host + ['host' => null]; + return; + } + $this->_proxy = compact('host', 'port', 'method', 'user', 'pass'); + } + + /** + * Set the resource to receive the request content. This resource must support fwrite. + * + * @param resource|bool $resource Resource or false to disable the resource use + * @return void + * @throws SocketException + */ + public function setContentResource($resource) + { + if ($resource === false) { + $this->_contentResource = null; + return; + } + if (!is_resource($resource)) { + throw new SocketException(__d('cake_dev', 'Invalid resource.')); + } + $this->_contentResource = $resource; + } + + /** + * Issues a GET request to the specified URI, query, and request. + * + * Using a string uri and an array of query string parameters: + * + * `$response = $http->get('http://google.com/search', array('q' => 'cakephp', 'client' => 'safari'));` + * + * Would do a GET request to `http://google.com/search?q=cakephp&client=safari` + * + * You could express the same thing using a uri array and query string parameters: + * + * ``` + * $response = $http->get( + * array('host' => 'google.com', 'path' => '/search'), + * array('q' => 'cakephp', 'client' => 'safari') + * ); + * ``` + * + * @param string|array $uri URI to request. Either a string uri, or a uri array, see HttpSocket::_parseUri() + * @param array $query Querystring parameters to append to URI + * @param array $request An indexed array with indexes such as 'method' or uri + * @return false|HttpSocketResponse Result of request, either false on failure or the response to the request. + */ + public function get($uri = null, $query = [], $request = []) + { + $uri = $this->_parseUri($uri, $this->config['request']['uri']); + if (isset($uri['query'])) { + $uri['query'] = array_merge($uri['query'], $query); + } else { + $uri['query'] = $query; + } + $uri = $this->_buildUri($uri); + + $request = Hash::merge(['method' => 'GET', 'uri' => $uri], $request); + return $this->request($request); + } + + /** + * Takes a $uri array and turns it into a fully qualified URL string + * + * @param string|array $uri Either A $uri array, or a request string. Will use $this->config if left empty. + * @param string $uriTemplate The Uri template/format to use. + * @return mixed A fully qualified URL formatted according to $uriTemplate, or false on failure + */ + protected function _buildUri($uri = [], $uriTemplate = '%scheme://%user:%pass@%host:%port/%path?%query#%fragment') + { + if (is_string($uri)) { + $uri = ['host' => $uri]; + } + $uri = $this->_parseUri($uri, true); + + if (!is_array($uri) || empty($uri)) { + return false; + } + + $uri['path'] = preg_replace('/^\//', null, $uri['path']); + $uri['query'] = http_build_query($uri['query'], '', '&'); + $uri['query'] = rtrim($uri['query'], '='); + $stripIfEmpty = [ + 'query' => '?%query', + 'fragment' => '#%fragment', + 'user' => '%user:%pass@', + 'host' => '%host:%port/' + ]; + + foreach ($stripIfEmpty as $key => $strip) { + if (empty($uri[$key])) { + $uriTemplate = str_replace($strip, null, $uriTemplate); + } + } + + $defaultPorts = ['http' => 80, 'https' => 443]; + if (array_key_exists($uri['scheme'], $defaultPorts) && $defaultPorts[$uri['scheme']] == $uri['port']) { + $uriTemplate = str_replace(':%port', null, $uriTemplate); + } + foreach ($uri as $property => $value) { + $uriTemplate = str_replace('%' . $property, $value, $uriTemplate); + } + + if ($uriTemplate === '/*') { + $uriTemplate = '*'; + } + return $uriTemplate; + } + + /** + * Issue the specified request. HttpSocket::get() and HttpSocket::post() wrap this + * method and provide a more granular interface. + * + * @param string|array $request Either an URI string, or an array defining host/uri + * @return false|HttpSocketResponse false on error, HttpSocketResponse on success + * @throws SocketException + */ + public function request($request = []) + { + $this->reset(false); + + if (is_string($request)) { + $request = ['uri' => $request]; + } else if (!is_array($request)) { + return false; + } + + if (!isset($request['uri'])) { + $request['uri'] = null; + } + $uri = $this->_parseUri($request['uri']); + if (!isset($uri['host'])) { + $host = $this->config['host']; + } + if (isset($request['host'])) { + $host = $request['host']; + unset($request['host']); + } + $request['uri'] = $this->url($request['uri']); + $request['uri'] = $this->_parseUri($request['uri'], true); + $this->request = Hash::merge($this->request, array_diff_key($this->config['request'], ['cookies' => true]), $request); + + $this->_configUri($this->request['uri']); + + $Host = $this->request['uri']['host']; + if (!empty($this->config['request']['cookies'][$Host])) { + if (!isset($this->request['cookies'])) { + $this->request['cookies'] = []; + } + if (!isset($request['cookies'])) { + $request['cookies'] = []; + } + $this->request['cookies'] = array_merge($this->request['cookies'], $this->config['request']['cookies'][$Host], $request['cookies']); + } + + if (isset($host)) { + $this->config['host'] = $host; + } + + $this->_setProxy(); + $this->request['proxy'] = $this->_proxy; + + $cookies = null; + + if (is_array($this->request['header'])) { + if (!empty($this->request['cookies'])) { + $cookies = $this->buildCookies($this->request['cookies']); + } + $scheme = ''; + $port = 0; + if (isset($this->request['uri']['scheme'])) { + $scheme = $this->request['uri']['scheme']; + } + if (isset($this->request['uri']['port'])) { + $port = $this->request['uri']['port']; + } + if (($scheme === 'http' && $port != 80) || + ($scheme === 'https' && $port != 443) || + ($port != 80 && $port != 443) + ) { + $Host .= ':' . $port; + } + $this->request['header'] = array_merge(compact('Host'), $this->request['header']); + } + + if (isset($this->request['uri']['user'], $this->request['uri']['pass'])) { + $this->configAuth('Basic', $this->request['uri']['user'], $this->request['uri']['pass']); + } else if (isset($this->request['auth'], $this->request['auth']['method'], $this->request['auth']['user'], $this->request['auth']['pass'])) { + $this->configAuth($this->request['auth']['method'], $this->request['auth']['user'], $this->request['auth']['pass']); + } + $authHeader = Hash::get($this->request, 'header.Authorization'); + if (empty($authHeader)) { + $this->_setAuth(); + $this->request['auth'] = $this->_auth; + } + + if (is_array($this->request['body'])) { + $this->request['body'] = http_build_query($this->request['body'], '', '&'); + } + + if (!empty($this->request['body']) && !isset($this->request['header']['Content-Type'])) { + $this->request['header']['Content-Type'] = 'application/x-www-form-urlencoded'; + } + + if (!empty($this->request['body']) && !isset($this->request['header']['Content-Length'])) { + $this->request['header']['Content-Length'] = strlen($this->request['body']); + } + if (isset($this->request['uri']['scheme']) && $this->request['uri']['scheme'] === 'https' && in_array($this->config['protocol'], [false, 'tcp'])) { + $this->config['protocol'] = 'ssl'; + } + + $connectionType = null; + if (isset($this->request['header']['Connection'])) { + $connectionType = $this->request['header']['Connection']; + } + $this->request['header'] = $this->_buildHeader($this->request['header']) . $cookies; + + if (empty($this->request['line'])) { + $this->request['line'] = $this->_buildRequestLine($this->request); + } + + if ($this->quirksMode === false && $this->request['line'] === false) { + return false; + } + + $this->request['raw'] = ''; + if ($this->request['line'] !== false) { + $this->request['raw'] = $this->request['line']; + } + + if ($this->request['header'] !== false) { + $this->request['raw'] .= $this->request['header']; + } + + $this->request['raw'] .= "\r\n"; + $this->request['raw'] .= $this->request['body']; + + // SSL context is set during the connect() method. + $this->write($this->request['raw']); + + $response = null; + $inHeader = true; + while (($data = $this->read()) !== false) { + if ($this->_contentResource) { + if ($inHeader) { + $response .= $data; + $pos = strpos($response, "\r\n\r\n"); + if ($pos !== false) { + $pos += 4; + $data = substr($response, $pos); + fwrite($this->_contentResource, $data); + + $response = substr($response, 0, $pos); + $inHeader = false; + } + } else { + fwrite($this->_contentResource, $data); + fflush($this->_contentResource); + } + } else { + $response .= $data; + } + } + + if ($connectionType === 'close') { + $this->disconnect(); + } + + list($plugin, $responseClass) = pluginSplit($this->responseClass, true); + App::uses($responseClass, $plugin . 'Network/Http'); + if (!class_exists($responseClass)) { + throw new SocketException(__d('cake_dev', 'Class %s not found.', $this->responseClass)); + } + $this->response = new $responseClass($response); + + if (!empty($this->response->cookies)) { + if (!isset($this->config['request']['cookies'][$Host])) { + $this->config['request']['cookies'][$Host] = []; + } + $this->config['request']['cookies'][$Host] = array_merge($this->config['request']['cookies'][$Host], $this->response->cookies); + } + + if ($this->request['redirect'] && $this->response->isRedirect()) { + $location = trim($this->response->getHeader('Location'), '='); + $request['uri'] = str_replace('%2F', '/', $location); + $request['redirect'] = is_int($this->request['redirect']) ? $this->request['redirect'] - 1 : $this->request['redirect']; + $this->response = $this->request($request); + } + + return $this->response; + } + + /** + * Resets the state of this HttpSocket instance to it's initial state (before CakeObject::__construct got executed) or does + * the same thing partially for the request and the response property only. + * + * @param bool $full If set to false only HttpSocket::response and HttpSocket::request are reset + * @return bool True on success + */ + public function reset($full = true) + { + static $initalState = []; + if (empty($initalState)) { + $initalState = get_class_vars(__CLASS__); + } + if (!$full) { + $this->request = $initalState['request']; + $this->response = $initalState['response']; + return true; + } + parent::reset($initalState); + return true; + } + + /** + * Normalizes URLs into a $uriTemplate. If no template is provided + * a default one will be used. Will generate the URL using the + * current config information. + * + * ### Usage: + * + * After configuring part of the request parameters, you can use url() to generate + * URLs. + * + * ``` + * $http = new HttpSocket('https://www.cakephp.org'); + * $url = $http->url('/search?q=bar'); + * ``` + * + * Would return `https://cakephp.org/search?q=bar` + * + * url() can also be used with custom templates: + * + * `$url = $http->url('http://www.cakephp/search?q=socket', '/%path?%query');` + * + * Would return `/search?q=socket`. + * + * @param string|array $url Either a string or array of URL options to create a URL with. + * @param string $uriTemplate A template string to use for URL formatting. + * @return mixed Either false on failure or a string containing the composed URL. + */ + public function url($url = null, $uriTemplate = null) + { + if ($url === null) { + $url = '/'; + } + if (is_string($url)) { + $scheme = $this->config['request']['uri']['scheme']; + if (is_array($scheme)) { + $scheme = $scheme[0]; + } + $port = $this->config['request']['uri']['port']; + if (is_array($port)) { + $port = $port[0]; + } + if ($url{0} === '/') { + $url = $this->config['request']['uri']['host'] . ':' . $port . $url; + } + if (!preg_match('/^.+:\/\/|\*|^\//', $url)) { + $url = $scheme . '://' . $url; + } + } else if (!is_array($url) && !empty($url)) { + return false; + } + + $base = array_merge($this->config['request']['uri'], ['scheme' => ['http', 'https'], 'port' => [80, 443]]); + $url = $this->_parseUri($url, $base); + + if (empty($url)) { + $url = $this->config['request']['uri']; + } + + if (!empty($uriTemplate)) { + return $this->_buildUri($url, $uriTemplate); + } + return $this->_buildUri($url); + } + + /** + * Set the proxy configuration and authentication + * + * @return void + * @throws SocketException + */ + protected function _setProxy() + { + if (empty($this->_proxy) || !isset($this->_proxy['host'], $this->_proxy['port'])) { + return; + } + $this->config['host'] = $this->_proxy['host']; + $this->config['port'] = $this->_proxy['port']; + $this->config['proxy'] = true; + + if (empty($this->_proxy['method']) || !isset($this->_proxy['user'], $this->_proxy['pass'])) { + return; + } + list($plugin, $authClass) = pluginSplit($this->_proxy['method'], true); + $authClass = Inflector::camelize($authClass) . 'Authentication'; + App::uses($authClass, $plugin . 'Network/Http'); + + if (!class_exists($authClass)) { + throw new SocketException(__d('cake_dev', 'Unknown authentication method for proxy.')); + } + if (!method_exists($authClass, 'proxyAuthentication')) { + throw new SocketException(__d('cake_dev', 'The %s does not support proxy authentication.', $authClass)); + } + call_user_func_array("$authClass::proxyAuthentication", [$this, &$this->_proxy]); + + if (!empty($this->request['header']['Proxy-Authorization'])) { + $this->config['proxyauth'] = $this->request['header']['Proxy-Authorization']; + if ($this->request['uri']['scheme'] === 'https') { + $this->request['header'] = Hash::remove($this->request['header'], 'Proxy-Authorization'); + } + } + } + + /** + * Builds cookie headers for a request. + * + * Cookies can either be in the format returned in responses, or + * a simple key => value pair. + * + * @param array $cookies Array of cookies to send with the request. + * @return string Cookie header string to be sent with the request. + */ + public function buildCookies($cookies) + { + $header = []; + foreach ($cookies as $name => $cookie) { + if (is_array($cookie)) { + $value = $this->_escapeToken($cookie['value'], [';']); + } else { + $value = $this->_escapeToken($cookie, [';']); + } + $header[] = $name . '=' . $value; + } + return $this->_buildHeader(['Cookie' => implode('; ', $header)], 'pragmatic'); + } + + /** + * Escapes a given $token according to RFC 2616 (HTTP 1.1 specs) + * + * @param string $token Token to escape + * @param array $chars Characters to escape + * @return string Escaped token + */ + protected function _escapeToken($token, $chars = null) + { + $regex = '/([' . implode('', $this->_tokenEscapeChars(true, $chars)) . '])/'; + $token = preg_replace($regex, '"\\1"', $token); + return $token; + } + + /** + * Gets escape chars according to RFC 2616 (HTTP 1.1 specs). + * + * @param bool $hex true to get them as HEX values, false otherwise + * @param array $chars Characters to escape + * @return array Escape chars + */ + protected function _tokenEscapeChars($hex = true, $chars = null) + { + if (!empty($chars)) { + $escape = $chars; + } else { + $escape = ['"', "(", ")", "<", ">", "@", ",", ";", ":", "\\", "/", "[", "]", "?", "=", "{", "}", " "]; + for ($i = 0; $i <= 31; $i++) { + $escape[] = chr($i); + } + $escape[] = chr(127); + } + + if (!$hex) { + return $escape; + } + foreach ($escape as $key => $char) { + $escape[$key] = '\\x' . str_pad(dechex(ord($char)), 2, '0', STR_PAD_LEFT); + } + return $escape; + } + + /** + * Builds the header. + * + * @param array $header Header to build + * @param string $mode Mode + * @return string Header built from array + */ + protected function _buildHeader($header, $mode = 'standard') + { + if (is_string($header)) { + return $header; + } else if (!is_array($header)) { + return false; + } + + $fieldsInHeader = []; + foreach ($header as $key => $value) { + $lowKey = strtolower($key); + if (array_key_exists($lowKey, $fieldsInHeader)) { + $header[$fieldsInHeader[$lowKey]] = $value; + unset($header[$key]); + } else { + $fieldsInHeader[$lowKey] = $key; + } + } + + $returnHeader = ''; + foreach ($header as $field => $contents) { + if (is_array($contents) && $mode === 'standard') { + $contents = implode(',', $contents); + } + foreach ((array)$contents as $content) { + $contents = preg_replace("/\r\n(?![\t ])/", "\r\n ", $content); + $field = $this->_escapeToken($field); + + $returnHeader .= $field . ': ' . $contents . "\r\n"; + } + } + return $returnHeader; + } + + /** + * Set authentication settings. + * + * Accepts two forms of parameters. If all you need is a username + password, as with + * Basic authentication you can do the following: + * + * ``` + * $http->configAuth('Basic', 'mark', 'secret'); + * ``` + * + * If you are using an authentication strategy that requires more inputs, like Digest authentication + * you can call `configAuth()` with an array of user information. + * + * ``` + * $http->configAuth('Digest', array( + * 'user' => 'mark', + * 'pass' => 'secret', + * 'realm' => 'my-realm', + * 'nonce' => 1235 + * )); + * ``` + * + * To remove any set authentication strategy, call `configAuth()` with no parameters: + * + * `$http->configAuth();` + * + * @param string $method Authentication method (ie. Basic, Digest). If empty, disable authentication + * @param string|array $user Username for authentication. Can be an array with settings to authentication class + * @param string $pass Password for authentication + * @return void + */ + public function configAuth($method, $user = null, $pass = null) + { + if (empty($method)) { + $this->_auth = []; + return; + } + if (is_array($user)) { + $this->_auth = [$method => $user]; + return; + } + $this->_auth = [$method => compact('user', 'pass')]; + } + + /** + * Set authentication in request + * + * @return void + * @throws SocketException + */ + protected function _setAuth() + { + if (empty($this->_auth)) { + return; + } + $method = key($this->_auth); + list($plugin, $authClass) = pluginSplit($method, true); + $authClass = Inflector::camelize($authClass) . 'Authentication'; + App::uses($authClass, $plugin . 'Network/Http'); + + if (!class_exists($authClass)) { + throw new SocketException(__d('cake_dev', 'Unknown authentication method.')); + } + if (!method_exists($authClass, 'authentication')) { + throw new SocketException(__d('cake_dev', 'The %s does not support authentication.', $authClass)); + } + call_user_func_array("$authClass::authentication", [$this, &$this->_auth[$method]]); + } + + /** + * Builds a request line according to HTTP/1.1 specs. Activate quirks mode to work outside specs. + * + * @param array $request Needs to contain a 'uri' key. Should also contain a 'method' key, otherwise defaults to GET. + * @return string Request line + * @throws SocketException + */ + protected function _buildRequestLine($request = []) + { + $asteriskMethods = ['OPTIONS']; + + if (is_string($request)) { + $isValid = preg_match("/(.+) (.+) (.+)\r\n/U", $request, $match); + if (!$this->quirksMode && (!$isValid || ($match[2] === '*' && !in_array($match[3], $asteriskMethods)))) { + throw new SocketException(__d('cake_dev', 'HttpSocket::_buildRequestLine - Passed an invalid request line string. Activate quirks mode to do this.')); + } + return $request; + } else if (!is_array($request)) { + return false; + } else if (!array_key_exists('uri', $request)) { + return false; + } + + $request['uri'] = $this->_parseUri($request['uri']); + $request += ['method' => 'GET']; + if (!empty($this->_proxy['host']) && $request['uri']['scheme'] !== 'https') { + $request['uri'] = $this->_buildUri($request['uri'], '%scheme://%host:%port/%path?%query'); + } else { + $request['uri'] = $this->_buildUri($request['uri'], '/%path?%query'); + } + + if (!$this->quirksMode && $request['uri'] === '*' && !in_array($request['method'], $asteriskMethods)) { + throw new SocketException(__d('cake_dev', 'HttpSocket::_buildRequestLine - The "*" asterisk character is only allowed for the following methods: %s. Activate quirks mode to work outside of HTTP/1.1 specs.', implode(',', $asteriskMethods))); + } + $version = isset($request['version']) ? $request['version'] : '1.1'; + return $request['method'] . ' ' . $request['uri'] . ' HTTP/' . $version . "\r\n"; + } + + /** + * Issues a HEAD request to the specified URI, query, and request. + * + * By definition HEAD request are identical to GET request except they return no response body. This means that all + * information and examples relevant to GET also applys to HEAD. + * + * @param string|array $uri URI to request. Either a string URI, or a URI array, see HttpSocket::_parseUri() + * @param array $query Querystring parameters to append to URI + * @param array $request An indexed array with indexes such as 'method' or uri + * @return false|HttpSocketResponse Result of request, either false on failure or the response to the request. + */ + public function head($uri = null, $query = [], $request = []) + { + $uri = $this->_parseUri($uri, $this->config['request']['uri']); + if (isset($uri['query'])) { + $uri['query'] = array_merge($uri['query'], $query); + } else { + $uri['query'] = $query; + } + $uri = $this->_buildUri($uri); + + $request = Hash::merge(['method' => 'HEAD', 'uri' => $uri], $request); + return $this->request($request); + } + + /** + * Issues a POST request to the specified URI, query, and request. + * + * `post()` can be used to post simple data arrays to a URL: + * + * ``` + * $response = $http->post('http://example.com', array( + * 'username' => 'batman', + * 'password' => 'bruce_w4yne' + * )); + * ``` + * + * @param string|array $uri URI to request. See HttpSocket::_parseUri() + * @param array $data Array of request body data keys and values. + * @param array $request An indexed array with indexes such as 'method' or uri + * @return false|HttpSocketResponse Result of request, either false on failure or the response to the request. + */ + public function post($uri = null, $data = [], $request = []) + { + $request = Hash::merge(['method' => 'POST', 'uri' => $uri, 'body' => $data], $request); + return $this->request($request); + } + + /** + * Issues a PUT request to the specified URI, query, and request. + * + * @param string|array $uri URI to request, See HttpSocket::_parseUri() + * @param array $data Array of request body data keys and values. + * @param array $request An indexed array with indexes such as 'method' or uri + * @return false|HttpSocketResponse Result of request + */ + public function put($uri = null, $data = [], $request = []) + { + $request = Hash::merge(['method' => 'PUT', 'uri' => $uri, 'body' => $data], $request); + return $this->request($request); + } + + /** + * Issues a PATCH request to the specified URI, query, and request. + * + * @param string|array $uri URI to request, See HttpSocket::_parseUri() + * @param array $data Array of request body data keys and values. + * @param array $request An indexed array with indexes such as 'method' or uri + * @return false|HttpSocketResponse Result of request + */ + public function patch($uri = null, $data = [], $request = []) + { + $request = Hash::merge(['method' => 'PATCH', 'uri' => $uri, 'body' => $data], $request); + return $this->request($request); + } + + /** + * Issues a DELETE request to the specified URI, query, and request. + * + * @param string|array $uri URI to request (see {@link _parseUri()}) + * @param array $data Array of request body data keys and values. + * @param array $request An indexed array with indexes such as 'method' or uri + * @return false|HttpSocketResponse Result of request + */ + public function delete($uri = null, $data = [], $request = []) + { + $request = Hash::merge(['method' => 'DELETE', 'uri' => $uri, 'body' => $data], $request); + return $this->request($request); + } } diff --git a/lib/Cake/Network/Http/HttpSocketResponse.php b/lib/Cake/Network/Http/HttpSocketResponse.php index 69c6da72..1b15149f 100755 --- a/lib/Cake/Network/Http/HttpSocketResponse.php +++ b/lib/Cake/Network/Http/HttpSocketResponse.php @@ -20,442 +20,460 @@ * * @package Cake.Network.Http */ -class HttpSocketResponse implements ArrayAccess { - -/** - * Body content - * - * @var string - */ - public $body = ''; - -/** - * Headers - * - * @var array - */ - public $headers = array(); - -/** - * Cookies - * - * @var array - */ - public $cookies = array(); - -/** - * HTTP version - * - * @var string - */ - public $httpVersion = 'HTTP/1.1'; - -/** - * Response code - * - * @var int - */ - public $code = 0; - -/** - * Reason phrase - * - * @var string - */ - public $reasonPhrase = ''; - -/** - * Pure raw content - * - * @var string - */ - public $raw = ''; - -/** - * Context data in the response. - * Contains SSL certificates for example. - * - * @var array - */ - public $context = array(); - -/** - * Constructor - * - * @param string $message Message to parse. - */ - public function __construct($message = null) { - if ($message !== null) { - $this->parseResponse($message); - } - } - -/** - * Body content - * - * @return string - */ - public function body() { - return (string)$this->body; - } - -/** - * Get header in case insensitive - * - * @param string $name Header name. - * @param array $headers Headers to format. - * @return mixed String if header exists or null - */ - public function getHeader($name, $headers = null) { - if (!is_array($headers)) { - $headers =& $this->headers; - } - if (isset($headers[$name])) { - return $headers[$name]; - } - foreach ($headers as $key => $value) { - if (strcasecmp($key, $name) === 0) { - return $value; - } - } - return null; - } - -/** - * If return is 200 (OK) - * - * @return bool - */ - public function isOk() { - return in_array($this->code, array(200, 201, 202, 203, 204, 205, 206)); - } - -/** - * If return is a valid 3xx (Redirection) - * - * @return bool - */ - public function isRedirect() { - return in_array($this->code, array(301, 302, 303, 307)) && $this->getHeader('Location') !== null; - } - -/** - * Parses the given message and breaks it down in parts. - * - * @param string $message Message to parse - * @return void - * @throws SocketException - */ - public function parseResponse($message) { - if (!is_string($message)) { - throw new SocketException(__d('cake_dev', 'Invalid response.')); - } - - if (!preg_match("/^(.+\r\n)(.*)(?<=\r\n)\r\n/Us", $message, $match)) { - throw new SocketException(__d('cake_dev', 'Invalid HTTP response.')); - } - - list(, $statusLine, $header) = $match; - $this->raw = $message; - $this->body = (string)substr($message, strlen($match[0])); - - if (preg_match("/(.+) ([0-9]{3})(?:\s+(\w.+))?\s*\r\n/DU", $statusLine, $match)) { - $this->httpVersion = $match[1]; - $this->code = $match[2]; - if (isset($match[3])) { - $this->reasonPhrase = $match[3]; - } - } - - $this->headers = $this->_parseHeader($header); - $transferEncoding = $this->getHeader('Transfer-Encoding'); - $decoded = $this->_decodeBody($this->body, $transferEncoding); - $this->body = $decoded['body']; - - if (!empty($decoded['header'])) { - $this->headers = $this->_parseHeader($this->_buildHeader($this->headers) . $this->_buildHeader($decoded['header'])); - } - - if (!empty($this->headers)) { - $this->cookies = $this->parseCookies($this->headers); - } - } - -/** - * Generic function to decode a $body with a given $encoding. Returns either an array with the keys - * 'body' and 'header' or false on failure. - * - * @param string $body A string containing the body to decode. - * @param string|bool $encoding Can be false in case no encoding is being used, or a string representing the encoding. - * @return mixed Array of response headers and body or false. - */ - protected function _decodeBody($body, $encoding = 'chunked') { - if (!is_string($body)) { - return false; - } - if (empty($encoding)) { - return array('body' => $body, 'header' => false); - } - $decodeMethod = '_decode' . Inflector::camelize(str_replace('-', '_', $encoding)) . 'Body'; - - if (!is_callable(array(&$this, $decodeMethod))) { - return array('body' => $body, 'header' => false); - } - return $this->{$decodeMethod}($body); - } - -/** - * Decodes a chunked message $body and returns either an array with the keys 'body' and 'header' or false as - * a result. - * - * @param string $body A string containing the chunked body to decode. - * @return mixed Array of response headers and body or false. - * @throws SocketException - */ - protected function _decodeChunkedBody($body) { - if (!is_string($body)) { - return false; - } - - $decodedBody = null; - $chunkLength = null; - - while ($chunkLength !== 0) { - if (!preg_match('/^([0-9a-f]+)[ ]*(?:;(.+)=(.+))?(?:\r\n|\n)/iU', $body, $match)) { - // Handle remaining invalid data as one big chunk. - preg_match('/^(.*?)\r\n/', $body, $invalidMatch); - $length = isset($invalidMatch[1]) ? strlen($invalidMatch[1]) : 0; - $match = array( - 0 => '', - 1 => dechex($length) - ); - } - $chunkSize = 0; - $hexLength = 0; - if (isset($match[0])) { - $chunkSize = $match[0]; - } - if (isset($match[1])) { - $hexLength = $match[1]; - } - - $chunkLength = hexdec($hexLength); - $body = substr($body, strlen($chunkSize)); - - $decodedBody .= substr($body, 0, $chunkLength); - if ($chunkLength) { - $body = substr($body, $chunkLength + strlen("\r\n")); - } - } - - $entityHeader = false; - if (!empty($body)) { - $entityHeader = $this->_parseHeader($body); - } - return array('body' => $decodedBody, 'header' => $entityHeader); - } - -/** - * Parses an array based header. - * - * @param array $header Header as an indexed array (field => value) - * @return array|bool Parsed header - */ - protected function _parseHeader($header) { - if (is_array($header)) { - return $header; - } elseif (!is_string($header)) { - return false; - } - - preg_match_all("/(.+):(.+)(?:\r\n|\$)/Uis", $header, $matches, PREG_SET_ORDER); - $lines = explode("\r\n", $header); - - $header = array(); - foreach ($lines as $line) { - if (strlen($line) === 0) { - continue; - } - $continuation = false; - $first = substr($line, 0, 1); - - // Multi-line header - if ($first === ' ' || $first === "\t") { - $value .= preg_replace("/\s+/", ' ', $line); - $continuation = true; - } elseif (strpos($line, ':') !== false) { - list($field, $value) = explode(':', $line, 2); - $field = $this->_unescapeToken($field); - } - - $value = trim($value); - if (!isset($header[$field]) || $continuation) { - $header[$field] = $value; - } else { - $header[$field] = array_merge((array)$header[$field], (array)$value); - } - } - return $header; - } - -/** - * Parses cookies in response headers. - * - * @param array $header Header array containing one ore more 'Set-Cookie' headers. - * @return mixed Either false on no cookies, or an array of cookies received. - */ - public function parseCookies($header) { - $cookieHeader = $this->getHeader('Set-Cookie', $header); - if (!$cookieHeader) { - return false; - } - - $cookies = array(); - foreach ((array)$cookieHeader as $cookie) { - if (strpos($cookie, '";"') !== false) { - $cookie = str_replace('";"', "{__cookie_replace__}", $cookie); - $parts = str_replace("{__cookie_replace__}", '";"', explode(';', $cookie)); - } else { - $parts = preg_split('/\;[ \t]*/', $cookie); - } - - $nameParts = explode('=', array_shift($parts), 2); - if (count($nameParts) < 2) { - $nameParts = array('', $nameParts[0]); - } - list($name, $value) = $nameParts; - $cookies[$name] = compact('value'); - - foreach ($parts as $part) { - if (strpos($part, '=') !== false) { - list($key, $value) = explode('=', $part); - } else { - $key = $part; - $value = true; - } - - $key = strtolower($key); - if (!isset($cookies[$name][$key])) { - $cookies[$name][$key] = $value; - } - } - } - return $cookies; - } - -/** - * Unescapes a given $token according to RFC 2616 (HTTP 1.1 specs) - * - * @param string $token Token to unescape. - * @param array $chars Characters to unescape. - * @return string Unescaped token - */ - protected function _unescapeToken($token, $chars = null) { - $regex = '/"([' . implode('', $this->_tokenEscapeChars(true, $chars)) . '])"/'; - $token = preg_replace($regex, '\\1', $token); - return $token; - } - -/** - * Gets escape chars according to RFC 2616 (HTTP 1.1 specs). - * - * @param bool $hex True to get them as HEX values, false otherwise. - * @param array $chars Characters to uescape. - * @return array Escape chars - */ - protected function _tokenEscapeChars($hex = true, $chars = null) { - if (!empty($chars)) { - $escape = $chars; - } else { - $escape = array('"', "(", ")", "<", ">", "@", ",", ";", ":", "\\", "/", "[", "]", "?", "=", "{", "}", " "); - for ($i = 0; $i <= 31; $i++) { - $escape[] = chr($i); - } - $escape[] = chr(127); - } - - if (!$hex) { - return $escape; - } - foreach ($escape as $key => $char) { - $escape[$key] = '\\x' . str_pad(dechex(ord($char)), 2, '0', STR_PAD_LEFT); - } - return $escape; - } - -/** - * ArrayAccess - Offset Exists - * - * @param string $offset Offset to check. - * @return bool - */ - public function offsetExists($offset) { - return in_array($offset, array('raw', 'status', 'header', 'body', 'cookies')); - } - -/** - * ArrayAccess - Offset Get - * - * @param string $offset Offset to get. - * @return mixed - */ - public function offsetGet($offset) { - switch ($offset) { - case 'raw': - $firstLineLength = strpos($this->raw, "\r\n") + 2; - if ($this->raw[$firstLineLength] === "\r") { - $header = null; - } else { - $header = substr($this->raw, $firstLineLength, strpos($this->raw, "\r\n\r\n") - $firstLineLength) . "\r\n"; - } - return array( - 'status-line' => $this->httpVersion . ' ' . $this->code . ' ' . $this->reasonPhrase . "\r\n", - 'header' => $header, - 'body' => $this->body, - 'response' => $this->raw - ); - case 'status': - return array( - 'http-version' => $this->httpVersion, - 'code' => $this->code, - 'reason-phrase' => $this->reasonPhrase - ); - case 'header': - return $this->headers; - case 'body': - return $this->body; - case 'cookies': - return $this->cookies; - } - return null; - } - -/** - * ArrayAccess - Offset Set - * - * @param string $offset Offset to set. - * @param mixed $value Value. - * @return void - */ - public function offsetSet($offset, $value) { - } - -/** - * ArrayAccess - Offset Unset - * - * @param string $offset Offset to unset. - * @return void - */ - public function offsetUnset($offset) { - } - -/** - * Instance as string - * - * @return string - */ - public function __toString() { - return $this->body(); - } +class HttpSocketResponse implements ArrayAccess +{ + + /** + * Body content + * + * @var string + */ + public $body = ''; + + /** + * Headers + * + * @var array + */ + public $headers = []; + + /** + * Cookies + * + * @var array + */ + public $cookies = []; + + /** + * HTTP version + * + * @var string + */ + public $httpVersion = 'HTTP/1.1'; + + /** + * Response code + * + * @var int + */ + public $code = 0; + + /** + * Reason phrase + * + * @var string + */ + public $reasonPhrase = ''; + + /** + * Pure raw content + * + * @var string + */ + public $raw = ''; + + /** + * Context data in the response. + * Contains SSL certificates for example. + * + * @var array + */ + public $context = []; + + /** + * Constructor + * + * @param string $message Message to parse. + */ + public function __construct($message = null) + { + if ($message !== null) { + $this->parseResponse($message); + } + } + + /** + * Parses the given message and breaks it down in parts. + * + * @param string $message Message to parse + * @return void + * @throws SocketException + */ + public function parseResponse($message) + { + if (!is_string($message)) { + throw new SocketException(__d('cake_dev', 'Invalid response.')); + } + + if (!preg_match("/^(.+\r\n)(.*)(?<=\r\n)\r\n/Us", $message, $match)) { + throw new SocketException(__d('cake_dev', 'Invalid HTTP response.')); + } + + list(, $statusLine, $header) = $match; + $this->raw = $message; + $this->body = (string)substr($message, strlen($match[0])); + + if (preg_match("/(.+) ([0-9]{3})(?:\s+(\w.+))?\s*\r\n/DU", $statusLine, $match)) { + $this->httpVersion = $match[1]; + $this->code = $match[2]; + if (isset($match[3])) { + $this->reasonPhrase = $match[3]; + } + } + + $this->headers = $this->_parseHeader($header); + $transferEncoding = $this->getHeader('Transfer-Encoding'); + $decoded = $this->_decodeBody($this->body, $transferEncoding); + $this->body = $decoded['body']; + + if (!empty($decoded['header'])) { + $this->headers = $this->_parseHeader($this->_buildHeader($this->headers) . $this->_buildHeader($decoded['header'])); + } + + if (!empty($this->headers)) { + $this->cookies = $this->parseCookies($this->headers); + } + } + + /** + * Parses an array based header. + * + * @param array $header Header as an indexed array (field => value) + * @return array|bool Parsed header + */ + protected function _parseHeader($header) + { + if (is_array($header)) { + return $header; + } else if (!is_string($header)) { + return false; + } + + preg_match_all("/(.+):(.+)(?:\r\n|\$)/Uis", $header, $matches, PREG_SET_ORDER); + $lines = explode("\r\n", $header); + + $header = []; + foreach ($lines as $line) { + if (strlen($line) === 0) { + continue; + } + $continuation = false; + $first = substr($line, 0, 1); + + // Multi-line header + if ($first === ' ' || $first === "\t") { + $value .= preg_replace("/\s+/", ' ', $line); + $continuation = true; + } else if (strpos($line, ':') !== false) { + list($field, $value) = explode(':', $line, 2); + $field = $this->_unescapeToken($field); + } + + $value = trim($value); + if (!isset($header[$field]) || $continuation) { + $header[$field] = $value; + } else { + $header[$field] = array_merge((array)$header[$field], (array)$value); + } + } + return $header; + } + + /** + * Unescapes a given $token according to RFC 2616 (HTTP 1.1 specs) + * + * @param string $token Token to unescape. + * @param array $chars Characters to unescape. + * @return string Unescaped token + */ + protected function _unescapeToken($token, $chars = null) + { + $regex = '/"([' . implode('', $this->_tokenEscapeChars(true, $chars)) . '])"/'; + $token = preg_replace($regex, '\\1', $token); + return $token; + } + + /** + * Gets escape chars according to RFC 2616 (HTTP 1.1 specs). + * + * @param bool $hex True to get them as HEX values, false otherwise. + * @param array $chars Characters to uescape. + * @return array Escape chars + */ + protected function _tokenEscapeChars($hex = true, $chars = null) + { + if (!empty($chars)) { + $escape = $chars; + } else { + $escape = ['"', "(", ")", "<", ">", "@", ",", ";", ":", "\\", "/", "[", "]", "?", "=", "{", "}", " "]; + for ($i = 0; $i <= 31; $i++) { + $escape[] = chr($i); + } + $escape[] = chr(127); + } + + if (!$hex) { + return $escape; + } + foreach ($escape as $key => $char) { + $escape[$key] = '\\x' . str_pad(dechex(ord($char)), 2, '0', STR_PAD_LEFT); + } + return $escape; + } + + /** + * Get header in case insensitive + * + * @param string $name Header name. + * @param array $headers Headers to format. + * @return mixed String if header exists or null + */ + public function getHeader($name, $headers = null) + { + if (!is_array($headers)) { + $headers =& $this->headers; + } + if (isset($headers[$name])) { + return $headers[$name]; + } + foreach ($headers as $key => $value) { + if (strcasecmp($key, $name) === 0) { + return $value; + } + } + return null; + } + + /** + * Generic function to decode a $body with a given $encoding. Returns either an array with the keys + * 'body' and 'header' or false on failure. + * + * @param string $body A string containing the body to decode. + * @param string|bool $encoding Can be false in case no encoding is being used, or a string representing the encoding. + * @return mixed Array of response headers and body or false. + */ + protected function _decodeBody($body, $encoding = 'chunked') + { + if (!is_string($body)) { + return false; + } + if (empty($encoding)) { + return ['body' => $body, 'header' => false]; + } + $decodeMethod = '_decode' . Inflector::camelize(str_replace('-', '_', $encoding)) . 'Body'; + + if (!is_callable([&$this, $decodeMethod])) { + return ['body' => $body, 'header' => false]; + } + return $this->{$decodeMethod}($body); + } + + /** + * Parses cookies in response headers. + * + * @param array $header Header array containing one ore more 'Set-Cookie' headers. + * @return mixed Either false on no cookies, or an array of cookies received. + */ + public function parseCookies($header) + { + $cookieHeader = $this->getHeader('Set-Cookie', $header); + if (!$cookieHeader) { + return false; + } + + $cookies = []; + foreach ((array)$cookieHeader as $cookie) { + if (strpos($cookie, '";"') !== false) { + $cookie = str_replace('";"', "{__cookie_replace__}", $cookie); + $parts = str_replace("{__cookie_replace__}", '";"', explode(';', $cookie)); + } else { + $parts = preg_split('/\;[ \t]*/', $cookie); + } + + $nameParts = explode('=', array_shift($parts), 2); + if (count($nameParts) < 2) { + $nameParts = ['', $nameParts[0]]; + } + list($name, $value) = $nameParts; + $cookies[$name] = compact('value'); + + foreach ($parts as $part) { + if (strpos($part, '=') !== false) { + list($key, $value) = explode('=', $part); + } else { + $key = $part; + $value = true; + } + + $key = strtolower($key); + if (!isset($cookies[$name][$key])) { + $cookies[$name][$key] = $value; + } + } + } + return $cookies; + } + + /** + * If return is 200 (OK) + * + * @return bool + */ + public function isOk() + { + return in_array($this->code, [200, 201, 202, 203, 204, 205, 206]); + } + + /** + * If return is a valid 3xx (Redirection) + * + * @return bool + */ + public function isRedirect() + { + return in_array($this->code, [301, 302, 303, 307]) && $this->getHeader('Location') !== null; + } + + /** + * ArrayAccess - Offset Exists + * + * @param string $offset Offset to check. + * @return bool + */ + public function offsetExists($offset) + { + return in_array($offset, ['raw', 'status', 'header', 'body', 'cookies']); + } + + /** + * ArrayAccess - Offset Get + * + * @param string $offset Offset to get. + * @return mixed + */ + public function offsetGet($offset) + { + switch ($offset) { + case 'raw': + $firstLineLength = strpos($this->raw, "\r\n") + 2; + if ($this->raw[$firstLineLength] === "\r") { + $header = null; + } else { + $header = substr($this->raw, $firstLineLength, strpos($this->raw, "\r\n\r\n") - $firstLineLength) . "\r\n"; + } + return [ + 'status-line' => $this->httpVersion . ' ' . $this->code . ' ' . $this->reasonPhrase . "\r\n", + 'header' => $header, + 'body' => $this->body, + 'response' => $this->raw + ]; + case 'status': + return [ + 'http-version' => $this->httpVersion, + 'code' => $this->code, + 'reason-phrase' => $this->reasonPhrase + ]; + case 'header': + return $this->headers; + case 'body': + return $this->body; + case 'cookies': + return $this->cookies; + } + return null; + } + + /** + * ArrayAccess - Offset Set + * + * @param string $offset Offset to set. + * @param mixed $value Value. + * @return void + */ + public function offsetSet($offset, $value) + { + } + + /** + * ArrayAccess - Offset Unset + * + * @param string $offset Offset to unset. + * @return void + */ + public function offsetUnset($offset) + { + } + + /** + * Instance as string + * + * @return string + */ + public function __toString() + { + return $this->body(); + } + + /** + * Body content + * + * @return string + */ + public function body() + { + return (string)$this->body; + } + + /** + * Decodes a chunked message $body and returns either an array with the keys 'body' and 'header' or false as + * a result. + * + * @param string $body A string containing the chunked body to decode. + * @return mixed Array of response headers and body or false. + * @throws SocketException + */ + protected function _decodeChunkedBody($body) + { + if (!is_string($body)) { + return false; + } + + $decodedBody = null; + $chunkLength = null; + + while ($chunkLength !== 0) { + if (!preg_match('/^([0-9a-f]+)[ ]*(?:;(.+)=(.+))?(?:\r\n|\n)/iU', $body, $match)) { + // Handle remaining invalid data as one big chunk. + preg_match('/^(.*?)\r\n/', $body, $invalidMatch); + $length = isset($invalidMatch[1]) ? strlen($invalidMatch[1]) : 0; + $match = [ + 0 => '', + 1 => dechex($length) + ]; + } + $chunkSize = 0; + $hexLength = 0; + if (isset($match[0])) { + $chunkSize = $match[0]; + } + if (isset($match[1])) { + $hexLength = $match[1]; + } + + $chunkLength = hexdec($hexLength); + $body = substr($body, strlen($chunkSize)); + + $decodedBody .= substr($body, 0, $chunkLength); + if ($chunkLength) { + $body = substr($body, $chunkLength + strlen("\r\n")); + } + } + + $entityHeader = false; + if (!empty($body)) { + $entityHeader = $this->_parseHeader($body); + } + return ['body' => $decodedBody, 'header' => $entityHeader]; + } } diff --git a/lib/Cake/Routing/Dispatcher.php b/lib/Cake/Routing/Dispatcher.php index 042a8ed8..0cdd2684 100755 --- a/lib/Cake/Routing/Dispatcher.php +++ b/lib/Cake/Routing/Dispatcher.php @@ -37,237 +37,247 @@ * * @package Cake.Routing */ -class Dispatcher implements CakeEventListener { +class Dispatcher implements CakeEventListener +{ -/** - * Event manager, used to handle dispatcher filters - * - * @var CakeEventManager - */ - protected $_eventManager; + /** + * Event manager, used to handle dispatcher filters + * + * @var CakeEventManager + */ + protected $_eventManager; -/** - * Constructor. - * - * @param string $base The base directory for the application. Writes `App.base` to Configure. - */ - public function __construct($base = false) { - if ($base !== false) { - Configure::write('App.base', $base); - } - } + /** + * Constructor. + * + * @param string $base The base directory for the application. Writes `App.base` to Configure. + */ + public function __construct($base = false) + { + if ($base !== false) { + Configure::write('App.base', $base); + } + } -/** - * Returns the CakeEventManager instance or creates one if none was - * created. Attaches the default listeners and filters - * - * @return CakeEventManager - */ - public function getEventManager() { - if (!$this->_eventManager) { - $this->_eventManager = new CakeEventManager(); - $this->_eventManager->attach($this); - $this->_attachFilters($this->_eventManager); - } - return $this->_eventManager; - } + /** + * Returns the list of events this object listens to. + * + * @return array + */ + public function implementedEvents() + { + return ['Dispatcher.beforeDispatch' => 'parseParams']; + } -/** - * Returns the list of events this object listens to. - * - * @return array - */ - public function implementedEvents() { - return array('Dispatcher.beforeDispatch' => 'parseParams'); - } + /** + * Dispatches and invokes given Request, handing over control to the involved controller. If the controller is set + * to autoRender, via Controller::$autoRender, then Dispatcher will render the view. + * + * Actions in CakePHP can be any public method on a controller, that is not declared in Controller. If you + * want controller methods to be public and in-accessible by URL, then prefix them with a `_`. + * For example `public function _loadPosts() { }` would not be accessible via URL. Private and protected methods + * are also not accessible via URL. + * + * If no controller of given name can be found, invoke() will throw an exception. + * If the controller is found, and the action is not found an exception will be thrown. + * + * @param CakeRequest $request Request object to dispatch. + * @param CakeResponse $response Response object to put the results of the dispatch into. + * @param array $additionalParams Settings array ("bare", "return") which is melded with the GET and POST params + * @return string|null if `$request['return']` is set then it returns response body, null otherwise + * @triggers Dispatcher.beforeDispatch $this, compact('request', 'response', 'additionalParams') + * @triggers Dispatcher.afterDispatch $this, compact('request', 'response') + * @throws MissingControllerException When the controller is missing. + */ + public function dispatch(CakeRequest $request, CakeResponse $response, $additionalParams = []) + { + $beforeEvent = new CakeEvent('Dispatcher.beforeDispatch', $this, compact('request', 'response', 'additionalParams')); + $this->getEventManager()->dispatch($beforeEvent); -/** - * Attaches all event listeners for this dispatcher instance. Loads the - * dispatcher filters from the configured locations. - * - * @param CakeEventManager $manager Event manager instance. - * @return void - * @throws MissingDispatcherFilterException - */ - protected function _attachFilters($manager) { - $filters = Configure::read('Dispatcher.filters'); - if (empty($filters)) { - return; - } + $request = $beforeEvent->data['request']; + if ($beforeEvent->result instanceof CakeResponse) { + if (isset($request->params['return'])) { + return $beforeEvent->result->body(); + } + $beforeEvent->result->send(); + return null; + } - foreach ($filters as $index => $filter) { - $settings = array(); - if (is_array($filter) && !is_int($index) && class_exists($index)) { - $settings = $filter; - $filter = $index; - } - if (is_string($filter)) { - $filter = array('callable' => $filter); - } - if (is_string($filter['callable'])) { - list($plugin, $callable) = pluginSplit($filter['callable'], true); - App::uses($callable, $plugin . 'Routing/Filter'); - if (!class_exists($callable)) { - throw new MissingDispatcherFilterException($callable); - } - $manager->attach(new $callable($settings)); - } else { - $on = strtolower($filter['on']); - $options = array(); - if (isset($filter['priority'])) { - $options = array('priority' => $filter['priority']); - } - $manager->attach($filter['callable'], 'Dispatcher.' . $on . 'Dispatch', $options); - } - } - } + $controller = $this->_getController($request, $response); -/** - * Dispatches and invokes given Request, handing over control to the involved controller. If the controller is set - * to autoRender, via Controller::$autoRender, then Dispatcher will render the view. - * - * Actions in CakePHP can be any public method on a controller, that is not declared in Controller. If you - * want controller methods to be public and in-accessible by URL, then prefix them with a `_`. - * For example `public function _loadPosts() { }` would not be accessible via URL. Private and protected methods - * are also not accessible via URL. - * - * If no controller of given name can be found, invoke() will throw an exception. - * If the controller is found, and the action is not found an exception will be thrown. - * - * @param CakeRequest $request Request object to dispatch. - * @param CakeResponse $response Response object to put the results of the dispatch into. - * @param array $additionalParams Settings array ("bare", "return") which is melded with the GET and POST params - * @return string|null if `$request['return']` is set then it returns response body, null otherwise - * @triggers Dispatcher.beforeDispatch $this, compact('request', 'response', 'additionalParams') - * @triggers Dispatcher.afterDispatch $this, compact('request', 'response') - * @throws MissingControllerException When the controller is missing. - */ - public function dispatch(CakeRequest $request, CakeResponse $response, $additionalParams = array()) { - $beforeEvent = new CakeEvent('Dispatcher.beforeDispatch', $this, compact('request', 'response', 'additionalParams')); - $this->getEventManager()->dispatch($beforeEvent); + if (!($controller instanceof Controller)) { + throw new MissingControllerException([ + 'class' => Inflector::camelize($request->params['controller']) . 'Controller', + 'plugin' => empty($request->params['plugin']) ? null : Inflector::camelize($request->params['plugin']) + ]); + } - $request = $beforeEvent->data['request']; - if ($beforeEvent->result instanceof CakeResponse) { - if (isset($request->params['return'])) { - return $beforeEvent->result->body(); - } - $beforeEvent->result->send(); - return null; - } + $response = $this->_invoke($controller, $request); + if (isset($request->params['return'])) { + return $response->body(); + } - $controller = $this->_getController($request, $response); + $afterEvent = new CakeEvent('Dispatcher.afterDispatch', $this, compact('request', 'response')); + $this->getEventManager()->dispatch($afterEvent); + $afterEvent->data['response']->send(); + } - if (!($controller instanceof Controller)) { - throw new MissingControllerException(array( - 'class' => Inflector::camelize($request->params['controller']) . 'Controller', - 'plugin' => empty($request->params['plugin']) ? null : Inflector::camelize($request->params['plugin']) - )); - } + /** + * Returns the CakeEventManager instance or creates one if none was + * created. Attaches the default listeners and filters + * + * @return CakeEventManager + */ + public function getEventManager() + { + if (!$this->_eventManager) { + $this->_eventManager = new CakeEventManager(); + $this->_eventManager->attach($this); + $this->_attachFilters($this->_eventManager); + } + return $this->_eventManager; + } - $response = $this->_invoke($controller, $request); - if (isset($request->params['return'])) { - return $response->body(); - } + /** + * Attaches all event listeners for this dispatcher instance. Loads the + * dispatcher filters from the configured locations. + * + * @param CakeEventManager $manager Event manager instance. + * @return void + * @throws MissingDispatcherFilterException + */ + protected function _attachFilters($manager) + { + $filters = Configure::read('Dispatcher.filters'); + if (empty($filters)) { + return; + } - $afterEvent = new CakeEvent('Dispatcher.afterDispatch', $this, compact('request', 'response')); - $this->getEventManager()->dispatch($afterEvent); - $afterEvent->data['response']->send(); - } + foreach ($filters as $index => $filter) { + $settings = []; + if (is_array($filter) && !is_int($index) && class_exists($index)) { + $settings = $filter; + $filter = $index; + } + if (is_string($filter)) { + $filter = ['callable' => $filter]; + } + if (is_string($filter['callable'])) { + list($plugin, $callable) = pluginSplit($filter['callable'], true); + App::uses($callable, $plugin . 'Routing/Filter'); + if (!class_exists($callable)) { + throw new MissingDispatcherFilterException($callable); + } + $manager->attach(new $callable($settings)); + } else { + $on = strtolower($filter['on']); + $options = []; + if (isset($filter['priority'])) { + $options = ['priority' => $filter['priority']]; + } + $manager->attach($filter['callable'], 'Dispatcher.' . $on . 'Dispatch', $options); + } + } + } -/** - * Initializes the components and models a controller will be using. - * Triggers the controller action, and invokes the rendering if Controller::$autoRender - * is true and echo's the output. Otherwise the return value of the controller - * action are returned. - * - * @param Controller $controller Controller to invoke - * @param CakeRequest $request The request object to invoke the controller for. - * @return CakeResponse the resulting response object - */ - protected function _invoke(Controller $controller, CakeRequest $request) { - $controller->constructClasses(); - $controller->startupProcess(); + /** + * Get controller to use, either plugin controller or application controller + * + * @param CakeRequest $request Request object + * @param CakeResponse $response Response for the controller. + * @return mixed name of controller if not loaded, or object if loaded + */ + protected function _getController($request, $response) + { + $ctrlClass = $this->_loadController($request); + if (!$ctrlClass) { + return false; + } + $reflection = new ReflectionClass($ctrlClass); + if ($reflection->isAbstract() || $reflection->isInterface()) { + return false; + } + return $reflection->newInstance($request, $response); + } - $response = $controller->response; - $render = true; - $result = $controller->invokeAction($request); - if ($result instanceof CakeResponse) { - $render = false; - $response = $result; - } + /** + * Load controller and return controller class name + * + * @param CakeRequest $request Request instance. + * @return string|bool Name of controller class name + */ + protected function _loadController($request) + { + $pluginName = $pluginPath = $controller = null; + if (!empty($request->params['plugin'])) { + $pluginName = $controller = Inflector::camelize($request->params['plugin']); + $pluginPath = $pluginName . '.'; + } + if (!empty($request->params['controller'])) { + $controller = Inflector::camelize($request->params['controller']); + } + if ($pluginPath . $controller) { + $class = $controller . 'Controller'; + App::uses('AppController', 'Controller'); + App::uses($pluginName . 'AppController', $pluginPath . 'Controller'); + App::uses($class, $pluginPath . 'Controller'); + if (class_exists($class)) { + return $class; + } + } + return false; + } - if ($render && $controller->autoRender) { - $response = $controller->render(); - } elseif (!($result instanceof CakeResponse) && $response->body() === null) { - $response->body($result); - } - $controller->shutdownProcess(); + /** + * Initializes the components and models a controller will be using. + * Triggers the controller action, and invokes the rendering if Controller::$autoRender + * is true and echo's the output. Otherwise the return value of the controller + * action are returned. + * + * @param Controller $controller Controller to invoke + * @param CakeRequest $request The request object to invoke the controller for. + * @return CakeResponse the resulting response object + */ + protected function _invoke(Controller $controller, CakeRequest $request) + { + $controller->constructClasses(); + $controller->startupProcess(); - return $response; - } + $response = $controller->response; + $render = true; + $result = $controller->invokeAction($request); + if ($result instanceof CakeResponse) { + $render = false; + $response = $result; + } -/** - * Applies Routing and additionalParameters to the request to be dispatched. - * If Routes have not been loaded they will be loaded, and app/Config/routes.php will be run. - * - * @param CakeEvent $event containing the request, response and additional params - * @return void - */ - public function parseParams($event) { - $request = $event->data['request']; - Router::setRequestInfo($request); - $params = Router::parse($request->url); - $request->addParams($params); + if ($render && $controller->autoRender) { + $response = $controller->render(); + } else if (!($result instanceof CakeResponse) && $response->body() === null) { + $response->body($result); + } + $controller->shutdownProcess(); - if (!empty($event->data['additionalParams'])) { - $request->addParams($event->data['additionalParams']); - } - } + return $response; + } -/** - * Get controller to use, either plugin controller or application controller - * - * @param CakeRequest $request Request object - * @param CakeResponse $response Response for the controller. - * @return mixed name of controller if not loaded, or object if loaded - */ - protected function _getController($request, $response) { - $ctrlClass = $this->_loadController($request); - if (!$ctrlClass) { - return false; - } - $reflection = new ReflectionClass($ctrlClass); - if ($reflection->isAbstract() || $reflection->isInterface()) { - return false; - } - return $reflection->newInstance($request, $response); - } + /** + * Applies Routing and additionalParameters to the request to be dispatched. + * If Routes have not been loaded they will be loaded, and app/Config/routes.php will be run. + * + * @param CakeEvent $event containing the request, response and additional params + * @return void + */ + public function parseParams($event) + { + $request = $event->data['request']; + Router::setRequestInfo($request); + $params = Router::parse($request->url); + $request->addParams($params); -/** - * Load controller and return controller class name - * - * @param CakeRequest $request Request instance. - * @return string|bool Name of controller class name - */ - protected function _loadController($request) { - $pluginName = $pluginPath = $controller = null; - if (!empty($request->params['plugin'])) { - $pluginName = $controller = Inflector::camelize($request->params['plugin']); - $pluginPath = $pluginName . '.'; - } - if (!empty($request->params['controller'])) { - $controller = Inflector::camelize($request->params['controller']); - } - if ($pluginPath . $controller) { - $class = $controller . 'Controller'; - App::uses('AppController', 'Controller'); - App::uses($pluginName . 'AppController', $pluginPath . 'Controller'); - App::uses($class, $pluginPath . 'Controller'); - if (class_exists($class)) { - return $class; - } - } - return false; - } + if (!empty($event->data['additionalParams'])) { + $request->addParams($event->data['additionalParams']); + } + } } diff --git a/lib/Cake/Routing/DispatcherFilter.php b/lib/Cake/Routing/DispatcherFilter.php index 89534041..73ae6144 100755 --- a/lib/Cake/Routing/DispatcherFilter.php +++ b/lib/Cake/Routing/DispatcherFilter.php @@ -7,10 +7,10 @@ * For full copyright and license information, please see the LICENSE.txt * Redistributions of files must retain the above copyright notice. * - * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org) - * @link https://cakephp.org CakePHP(tm) Project - * @package Cake.Routing - * @since CakePHP(tm) v 2.2 + * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org) + * @link https://cakephp.org CakePHP(tm) Project + * @package Cake.Routing + * @since CakePHP(tm) v 2.2 * @license https://opensource.org/licenses/mit-license.php MIT License */ @@ -23,78 +23,83 @@ * * @package Cake.Routing */ -abstract class DispatcherFilter implements CakeEventListener { +abstract class DispatcherFilter implements CakeEventListener +{ -/** - * Default priority for all methods in this filter - * - * @var int - */ - public $priority = 10; + /** + * Default priority for all methods in this filter + * + * @var int + */ + public $priority = 10; -/** - * Settings for this filter - * - * @var array - */ - public $settings = array(); + /** + * Settings for this filter + * + * @var array + */ + public $settings = []; -/** - * Constructor. - * - * @param array $settings Configuration settings for the filter. - */ - public function __construct($settings = array()) { - $this->settings = Hash::merge($this->settings, $settings); - } + /** + * Constructor. + * + * @param array $settings Configuration settings for the filter. + */ + public function __construct($settings = []) + { + $this->settings = Hash::merge($this->settings, $settings); + } -/** - * Returns the list of events this filter listens to. - * Dispatcher notifies 2 different events `Dispatcher.before` and `Dispatcher.after`. - * By default this class will attach `preDispatch` and `postDispatch` method respectively. - * - * Override this method at will to only listen to the events you are interested in. - * - * @return array - */ - public function implementedEvents() { - return array( - 'Dispatcher.beforeDispatch' => array('callable' => 'beforeDispatch', 'priority' => $this->priority), - 'Dispatcher.afterDispatch' => array('callable' => 'afterDispatch', 'priority' => $this->priority), - ); - } + /** + * Returns the list of events this filter listens to. + * Dispatcher notifies 2 different events `Dispatcher.before` and `Dispatcher.after`. + * By default this class will attach `preDispatch` and `postDispatch` method respectively. + * + * Override this method at will to only listen to the events you are interested in. + * + * @return array + */ + public function implementedEvents() + { + return [ + 'Dispatcher.beforeDispatch' => ['callable' => 'beforeDispatch', 'priority' => $this->priority], + 'Dispatcher.afterDispatch' => ['callable' => 'afterDispatch', 'priority' => $this->priority], + ]; + } -/** - * Method called before the controller is instantiated and called to serve a request. - * If used with default priority, it will be called after the Router has parsed the - * URL and set the routing params into the request object. - * - * If a CakeResponse object instance is returned, it will be served at the end of the - * event cycle, not calling any controller as a result. This will also have the effect of - * not calling the after event in the dispatcher. - * - * If false is returned, the event will be stopped and no more listeners will be notified. - * Alternatively you can call `$event->stopPropagation()` to achieve the same result. - * - * @param CakeEvent $event container object having the `request`, `response` and `additionalParams` - * keys in the data property. - * @return CakeResponse|bool - */ - public function beforeDispatch(CakeEvent $event) { - } + /** + * Method called before the controller is instantiated and called to serve a request. + * If used with default priority, it will be called after the Router has parsed the + * URL and set the routing params into the request object. + * + * If a CakeResponse object instance is returned, it will be served at the end of the + * event cycle, not calling any controller as a result. This will also have the effect of + * not calling the after event in the dispatcher. + * + * If false is returned, the event will be stopped and no more listeners will be notified. + * Alternatively you can call `$event->stopPropagation()` to achieve the same result. + * + * @param CakeEvent $event container object having the `request`, `response` and `additionalParams` + * keys in the data property. + * @return CakeResponse|bool + */ + public function beforeDispatch(CakeEvent $event) + { + } -/** - * Method called after the controller served a request and generated a response. - * It is possible to alter the response object at this point as it is not sent to the - * client yet. - * - * If false is returned, the event will be stopped and no more listeners will be notified. - * Alternatively you can call `$event->stopPropagation()` to achieve the same result. - * - * @param CakeEvent $event container object having the `request` and `response` - * keys in the data property. - * @return mixed boolean to stop the event dispatching or null to continue - */ - public function afterDispatch(CakeEvent $event) { - } + /** + * Method called after the controller served a request and generated a response. + * It is possible to alter the response object at this point as it is not sent to the + * client yet. + * + * If false is returned, the event will be stopped and no more listeners will be notified. + * Alternatively you can call `$event->stopPropagation()` to achieve the same result. + * + * @param CakeEvent $event container object having the `request` and `response` + * keys in the data property. + * @return mixed boolean to stop the event dispatching or null to continue + */ + public function afterDispatch(CakeEvent $event) + { + } } diff --git a/lib/Cake/Routing/Filter/AssetDispatcher.php b/lib/Cake/Routing/Filter/AssetDispatcher.php index c1e74066..fa654528 100755 --- a/lib/Cake/Routing/Filter/AssetDispatcher.php +++ b/lib/Cake/Routing/Filter/AssetDispatcher.php @@ -7,10 +7,10 @@ * For full copyright and license information, please see the LICENSE.txt * Redistributions of files must retain the above copyright notice. * - * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org) - * @link https://cakephp.org CakePHP(tm) Project - * @package Cake.Routing - * @since CakePHP(tm) v 2.2 + * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org) + * @link https://cakephp.org CakePHP(tm) Project + * @package Cake.Routing + * @since CakePHP(tm) v 2.2 * @license https://opensource.org/licenses/mit-license.php MIT License */ @@ -22,147 +22,152 @@ * * @package Cake.Routing.Filter */ -class AssetDispatcher extends DispatcherFilter { - -/** - * Default priority for all methods in this filter - * This filter should run before the request gets parsed by router - * - * @var int - */ - public $priority = 9; - -/** - * Checks if a requested asset exists and sends it to the browser - * - * @param CakeEvent $event containing the request and response object - * @return mixed The resulting response. - * @throws NotFoundException When asset not found - */ - public function beforeDispatch(CakeEvent $event) { - $url = urldecode($event->data['request']->url); - if (strpos($url, '..') !== false || strpos($url, '.') === false) { - return null; - } - - if ($result = $this->_filterAsset($event)) { - $event->stopPropagation(); - return $result; - } - - $assetFile = $this->_getAssetFile($url); - if ($assetFile === null || !file_exists($assetFile)) { - return null; - } - $response = $event->data['response']; - $event->stopPropagation(); - - $response->modified(filemtime($assetFile)); - if ($response->checkNotModified($event->data['request'])) { - return $response; - } - - $pathSegments = explode('.', $url); - $ext = array_pop($pathSegments); - - $this->_deliverAsset($response, $assetFile, $ext); - return $response; - } - -/** - * Checks if the client is requesting a filtered asset and runs the corresponding - * filter if any is configured - * - * @param CakeEvent $event containing the request and response object - * @return CakeResponse if the client is requesting a recognized asset, null otherwise - */ - protected function _filterAsset(CakeEvent $event) { - $url = $event->data['request']->url; - $response = $event->data['response']; - $filters = Configure::read('Asset.filter'); - $isCss = ( - strpos($url, 'ccss/') === 0 || - preg_match('#^(theme/([^/]+)/ccss/)|(([^/]+)(?statusCode(404); - return $response; - } - - if ($isCss) { - include WWW_ROOT . DS . $filters['css']; - return $response; - } - - if ($isJs) { - include WWW_ROOT . DS . $filters['js']; - return $response; - } - } - -/** - * Builds asset file path based off url - * - * @param string $url URL - * @return string Absolute path for asset file - */ - protected function _getAssetFile($url) { - $parts = explode('/', $url); - if ($parts[0] === 'theme') { - $themeName = $parts[1]; - unset($parts[0], $parts[1]); - $fileFragment = implode(DS, $parts); - $path = App::themePath($themeName) . 'webroot' . DS; - return $path . $fileFragment; - } - - $plugin = Inflector::camelize($parts[0]); - if ($plugin && CakePlugin::loaded($plugin)) { - unset($parts[0]); - $fileFragment = implode(DS, $parts); - $pluginWebroot = CakePlugin::path($plugin) . 'webroot' . DS; - return $pluginWebroot . $fileFragment; - } - } - -/** - * Sends an asset file to the client - * - * @param CakeResponse $response The response object to use. - * @param string $assetFile Path to the asset file in the file system - * @param string $ext The extension of the file to determine its mime type - * @return void - */ - protected function _deliverAsset(CakeResponse $response, $assetFile, $ext) { - ob_start(); - $compressionEnabled = Configure::read('Asset.compress') && $response->compress(); - if ($response->type($ext) === $ext) { - $contentType = 'application/octet-stream'; - $agent = env('HTTP_USER_AGENT'); - if (preg_match('%Opera(/| )([0-9].[0-9]{1,2})%', $agent) || preg_match('/MSIE ([0-9].[0-9]{1,2})/', $agent)) { - $contentType = 'application/octetstream'; - } - $response->type($contentType); - } - $response->length(false); - $response->cache(filemtime($assetFile)); - $response->send(); - ob_clean(); - - if ($ext === 'css' || $ext === 'js') { - include $assetFile; - } else { - readfile($assetFile); - } - - if ($compressionEnabled) { - ob_end_flush(); - } - } +class AssetDispatcher extends DispatcherFilter +{ + + /** + * Default priority for all methods in this filter + * This filter should run before the request gets parsed by router + * + * @var int + */ + public $priority = 9; + + /** + * Checks if a requested asset exists and sends it to the browser + * + * @param CakeEvent $event containing the request and response object + * @return mixed The resulting response. + * @throws NotFoundException When asset not found + */ + public function beforeDispatch(CakeEvent $event) + { + $url = urldecode($event->data['request']->url); + if (strpos($url, '..') !== false || strpos($url, '.') === false) { + return null; + } + + if ($result = $this->_filterAsset($event)) { + $event->stopPropagation(); + return $result; + } + + $assetFile = $this->_getAssetFile($url); + if ($assetFile === null || !file_exists($assetFile)) { + return null; + } + $response = $event->data['response']; + $event->stopPropagation(); + + $response->modified(filemtime($assetFile)); + if ($response->checkNotModified($event->data['request'])) { + return $response; + } + + $pathSegments = explode('.', $url); + $ext = array_pop($pathSegments); + + $this->_deliverAsset($response, $assetFile, $ext); + return $response; + } + + /** + * Checks if the client is requesting a filtered asset and runs the corresponding + * filter if any is configured + * + * @param CakeEvent $event containing the request and response object + * @return CakeResponse if the client is requesting a recognized asset, null otherwise + */ + protected function _filterAsset(CakeEvent $event) + { + $url = $event->data['request']->url; + $response = $event->data['response']; + $filters = Configure::read('Asset.filter'); + $isCss = ( + strpos($url, 'ccss/') === 0 || + preg_match('#^(theme/([^/]+)/ccss/)|(([^/]+)(?statusCode(404); + return $response; + } + + if ($isCss) { + include WWW_ROOT . DS . $filters['css']; + return $response; + } + + if ($isJs) { + include WWW_ROOT . DS . $filters['js']; + return $response; + } + } + + /** + * Builds asset file path based off url + * + * @param string $url URL + * @return string Absolute path for asset file + */ + protected function _getAssetFile($url) + { + $parts = explode('/', $url); + if ($parts[0] === 'theme') { + $themeName = $parts[1]; + unset($parts[0], $parts[1]); + $fileFragment = implode(DS, $parts); + $path = App::themePath($themeName) . 'webroot' . DS; + return $path . $fileFragment; + } + + $plugin = Inflector::camelize($parts[0]); + if ($plugin && CakePlugin::loaded($plugin)) { + unset($parts[0]); + $fileFragment = implode(DS, $parts); + $pluginWebroot = CakePlugin::path($plugin) . 'webroot' . DS; + return $pluginWebroot . $fileFragment; + } + } + + /** + * Sends an asset file to the client + * + * @param CakeResponse $response The response object to use. + * @param string $assetFile Path to the asset file in the file system + * @param string $ext The extension of the file to determine its mime type + * @return void + */ + protected function _deliverAsset(CakeResponse $response, $assetFile, $ext) + { + ob_start(); + $compressionEnabled = Configure::read('Asset.compress') && $response->compress(); + if ($response->type($ext) === $ext) { + $contentType = 'application/octet-stream'; + $agent = env('HTTP_USER_AGENT'); + if (preg_match('%Opera(/| )([0-9].[0-9]{1,2})%', $agent) || preg_match('/MSIE ([0-9].[0-9]{1,2})/', $agent)) { + $contentType = 'application/octetstream'; + } + $response->type($contentType); + } + $response->length(false); + $response->cache(filemtime($assetFile)); + $response->send(); + ob_clean(); + + if ($ext === 'css' || $ext === 'js') { + include $assetFile; + } else { + readfile($assetFile); + } + + if ($compressionEnabled) { + ob_end_flush(); + } + } } diff --git a/lib/Cake/Routing/Filter/CacheDispatcher.php b/lib/Cake/Routing/Filter/CacheDispatcher.php index d7d67f3e..98f6983a 100755 --- a/lib/Cake/Routing/Filter/CacheDispatcher.php +++ b/lib/Cake/Routing/Filter/CacheDispatcher.php @@ -21,53 +21,55 @@ * * @package Cake.Routing.Filter */ -class CacheDispatcher extends DispatcherFilter { +class CacheDispatcher extends DispatcherFilter +{ -/** - * Default priority for all methods in this filter - * This filter should run before the request gets parsed by router - * - * @var int - */ - public $priority = 9; + /** + * Default priority for all methods in this filter + * This filter should run before the request gets parsed by router + * + * @var int + */ + public $priority = 9; -/** - * Checks whether the response was cached and set the body accordingly. - * - * @param CakeEvent $event containing the request and response object - * @return CakeResponse with cached content if found, null otherwise - */ - public function beforeDispatch(CakeEvent $event) { - if (Configure::read('Cache.check') !== true) { - return null; - } + /** + * Checks whether the response was cached and set the body accordingly. + * + * @param CakeEvent $event containing the request and response object + * @return CakeResponse with cached content if found, null otherwise + */ + public function beforeDispatch(CakeEvent $event) + { + if (Configure::read('Cache.check') !== true) { + return null; + } - $path = $event->data['request']->here(); - if ($path === '/') { - $path = 'home'; - } - $prefix = Configure::read('Cache.viewPrefix'); - if ($prefix) { - $path = $prefix . '_' . $path; - } - $path = strtolower(Inflector::slug($path)); + $path = $event->data['request']->here(); + if ($path === '/') { + $path = 'home'; + } + $prefix = Configure::read('Cache.viewPrefix'); + if ($prefix) { + $path = $prefix . '_' . $path; + } + $path = strtolower(Inflector::slug($path)); - $filename = CACHE . 'views' . DS . $path . '.php'; + $filename = CACHE . 'views' . DS . $path . '.php'; - if (!file_exists($filename)) { - $filename = CACHE . 'views' . DS . $path . '_index.php'; - } - if (file_exists($filename)) { - $controller = null; - $view = new View($controller); - $view->response = $event->data['response']; - $result = $view->renderCache($filename, microtime(true)); - if ($result !== false) { - $event->stopPropagation(); - $event->data['response']->body($result); - return $event->data['response']; - } - } - } + if (!file_exists($filename)) { + $filename = CACHE . 'views' . DS . $path . '_index.php'; + } + if (file_exists($filename)) { + $controller = null; + $view = new View($controller); + $view->response = $event->data['response']; + $result = $view->renderCache($filename, microtime(true)); + if ($result !== false) { + $event->stopPropagation(); + $event->data['response']->body($result); + return $event->data['response']; + } + } + } } diff --git a/lib/Cake/Routing/Route/CakeRoute.php b/lib/Cake/Routing/Route/CakeRoute.php index 85dbb59c..636fb658 100755 --- a/lib/Cake/Routing/Route/CakeRoute.php +++ b/lib/Cake/Routing/Route/CakeRoute.php @@ -24,543 +24,555 @@ * * @package Cake.Routing.Route */ -class CakeRoute { - -/** - * An array of named segments in a Route. - * `/:controller/:action/:id` has 3 key elements - * - * @var array - */ - public $keys = array(); - -/** - * An array of additional parameters for the Route. - * - * @var array - */ - public $options = array(); - -/** - * Default parameters for a Route - * - * @var array - */ - public $defaults = array(); - -/** - * The routes template string. - * - * @var string - */ - public $template = null; - -/** - * Is this route a greedy route? Greedy routes have a `/*` in their - * template - * - * @var string - */ - protected $_greedy = false; - -/** - * The compiled route regular expression - * - * @var string - */ - protected $_compiledRoute = null; - -/** - * HTTP header shortcut map. Used for evaluating header-based route expressions. - * - * @var array - */ - protected $_headerMap = array( - 'type' => 'content_type', - 'method' => 'request_method', - 'server' => 'server_name' - ); - -/** - * Constructor for a Route - * - * @param string $template Template string with parameter placeholders - * @param array $defaults Array of defaults for the route. - * @param array $options Array of additional options for the Route - */ - public function __construct($template, $defaults = array(), $options = array()) { - $this->template = $template; - $this->defaults = (array)$defaults; - $this->options = (array)$options; - } - -/** - * Check if a Route has been compiled into a regular expression. - * - * @return bool - */ - public function compiled() { - return !empty($this->_compiledRoute); - } - -/** - * Compiles the route's regular expression. - * - * Modifies defaults property so all necessary keys are set - * and populates $this->names with the named routing elements. - * - * @return array Returns a string regular expression of the compiled route. - */ - public function compile() { - if ($this->compiled()) { - return $this->_compiledRoute; - } - $this->_writeRoute(); - return $this->_compiledRoute; - } - -/** - * Builds a route regular expression. - * - * Uses the template, defaults and options properties to compile a - * regular expression that can be used to parse request strings. - * - * @return void - */ - protected function _writeRoute() { - if (empty($this->template) || ($this->template === '/')) { - $this->_compiledRoute = '#^/*$#'; - $this->keys = array(); - return; - } - $route = $this->template; - $names = $routeParams = array(); - $parsed = preg_quote($this->template, '#'); - - preg_match_all('#:([A-Za-z0-9_-]+[A-Z0-9a-z])#', $route, $namedElements); - foreach ($namedElements[1] as $i => $name) { - $search = '\\' . $namedElements[0][$i]; - if (isset($this->options[$name])) { - $option = null; - if ($name !== 'plugin' && array_key_exists($name, $this->defaults)) { - $option = '?'; - } - $slashParam = '/\\' . $namedElements[0][$i]; - if (strpos($parsed, $slashParam) !== false) { - $routeParams[$slashParam] = '(?:/(?P<' . $name . '>' . $this->options[$name] . ')' . $option . ')' . $option; - } else { - $routeParams[$search] = '(?:(?P<' . $name . '>' . $this->options[$name] . ')' . $option . ')' . $option; - } - } else { - $routeParams[$search] = '(?:(?P<' . $name . '>[^/]+))'; - } - $names[] = $name; - } - if (preg_match('#\/\*\*$#', $route)) { - $parsed = preg_replace('#/\\\\\*\\\\\*$#', '(?:/(?P<_trailing_>.*))?', $parsed); - $this->_greedy = true; - } - if (preg_match('#\/\*$#', $route)) { - $parsed = preg_replace('#/\\\\\*$#', '(?:/(?P<_args_>.*))?', $parsed); - $this->_greedy = true; - } - krsort($routeParams); - $parsed = str_replace(array_keys($routeParams), array_values($routeParams), $parsed); - $this->_compiledRoute = '#^' . $parsed . '[/]*$#'; - $this->keys = $names; - - // Remove defaults that are also keys. They can cause match failures - foreach ($this->keys as $key) { - unset($this->defaults[$key]); - } - - $keys = $this->keys; - sort($keys); - $this->keys = array_reverse($keys); - } - -/** - * Checks to see if the given URL can be parsed by this route. - * - * If the route can be parsed an array of parameters will be returned; if not - * false will be returned. String URLs are parsed if they match a routes regular expression. - * - * @param string $url The URL to attempt to parse. - * @return mixed Boolean false on failure, otherwise an array or parameters - */ - public function parse($url) { - if (!$this->compiled()) { - $this->compile(); - } - if (!preg_match($this->_compiledRoute, urldecode($url), $route)) { - return false; - } - foreach ($this->defaults as $key => $val) { - $key = (string)$key; - if ($key[0] === '[' && preg_match('/^\[(\w+)\]$/', $key, $header)) { - if (isset($this->_headerMap[$header[1]])) { - $header = $this->_headerMap[$header[1]]; - } else { - $header = 'http_' . $header[1]; - } - $header = strtoupper($header); - - $val = (array)$val; - $h = false; - - foreach ($val as $v) { - if (env($header) === $v) { - $h = true; - } - } - if (!$h) { - return false; - } - } - } - array_shift($route); - $count = count($this->keys); - for ($i = 0; $i <= $count; $i++) { - unset($route[$i]); - } - $route['pass'] = $route['named'] = array(); - - // Assign defaults, set passed args to pass - foreach ($this->defaults as $key => $value) { - if (isset($route[$key])) { - continue; - } - if (is_int($key)) { - $route['pass'][] = $value; - continue; - } - $route[$key] = $value; - } - - if (isset($route['_args_'])) { - list($pass, $named) = $this->_parseArgs($route['_args_'], $route); - $route['pass'] = array_merge($route['pass'], $pass); - $route['named'] = $named; - unset($route['_args_']); - } - - if (isset($route['_trailing_'])) { - $route['pass'][] = $route['_trailing_']; - unset($route['_trailing_']); - } - - // restructure 'pass' key route params - if (isset($this->options['pass'])) { - $j = count($this->options['pass']); - while ($j--) { - if (isset($route[$this->options['pass'][$j]])) { - array_unshift($route['pass'], $route[$this->options['pass'][$j]]); - } - } - } - return $route; - } - -/** - * Parse passed and Named parameters into a list of passed args, and a hash of named parameters. - * The local and global configuration for named parameters will be used. - * - * @param string $args A string with the passed & named params. eg. /1/page:2 - * @param string $context The current route context, which should contain controller/action keys. - * @return array Array of ($pass, $named) - */ - protected function _parseArgs($args, $context) { - $pass = $named = array(); - $args = explode('/', $args); - - $namedConfig = Router::namedConfig(); - $greedy = $namedConfig['greedyNamed']; - $rules = $namedConfig['rules']; - if (!empty($this->options['named'])) { - $greedy = isset($this->options['greedyNamed']) && $this->options['greedyNamed'] === true; - foreach ((array)$this->options['named'] as $key => $val) { - if (is_numeric($key)) { - $rules[$val] = true; - continue; - } - $rules[$key] = $val; - } - } - - foreach ($args as $param) { - if (empty($param) && $param !== '0' && $param !== 0) { - continue; - } - - $separatorIsPresent = strpos($param, $namedConfig['separator']) !== false; - if ((!isset($this->options['named']) || !empty($this->options['named'])) && $separatorIsPresent) { - list($key, $val) = explode($namedConfig['separator'], $param, 2); - $hasRule = isset($rules[$key]); - $passIt = (!$hasRule && !$greedy) || ($hasRule && !$this->_matchNamed($val, $rules[$key], $context)); - if ($passIt) { - $pass[] = $param; - } else { - if (preg_match_all('/\[([A-Za-z0-9_-]+)?\]/', $key, $matches, PREG_SET_ORDER)) { - $matches = array_reverse($matches); - $parts = explode('[', $key); - $key = array_shift($parts); - $arr = $val; - foreach ($matches as $match) { - if (empty($match[1])) { - $arr = array($arr); - } else { - $arr = array( - $match[1] => $arr - ); - } - } - $val = $arr; - } - $named = array_merge_recursive($named, array($key => $val)); - } - } else { - $pass[] = $param; - } - } - return array($pass, $named); - } - -/** - * Check if a named parameter matches the current rules. - * - * Return true if a given named $param's $val matches a given $rule depending on $context. - * Currently implemented rule types are controller, action and match that can be combined with each other. - * - * @param string $val The value of the named parameter - * @param array $rule The rule(s) to apply, can also be a match string - * @param string $context An array with additional context information (controller / action) - * @return bool - */ - protected function _matchNamed($val, $rule, $context) { - if ($rule === true || $rule === false) { - return $rule; - } - if (is_string($rule)) { - $rule = array('match' => $rule); - } - if (!is_array($rule)) { - return false; - } - - $controllerMatches = ( - !isset($rule['controller'], $context['controller']) || - in_array($context['controller'], (array)$rule['controller']) - ); - if (!$controllerMatches) { - return false; - } - $actionMatches = ( - !isset($rule['action'], $context['action']) || - in_array($context['action'], (array)$rule['action']) - ); - if (!$actionMatches) { - return false; - } - return (!isset($rule['match']) || preg_match('/' . $rule['match'] . '/', $val)); - } - -/** - * Apply persistent parameters to a URL array. Persistent parameters are a special - * key used during route creation to force route parameters to persist when omitted from - * a URL array. - * - * @param array $url The array to apply persistent parameters to. - * @param array $params An array of persistent values to replace persistent ones. - * @return array An array with persistent parameters applied. - */ - public function persistParams($url, $params) { - if (empty($this->options['persist']) || !is_array($this->options['persist'])) { - return $url; - } - foreach ($this->options['persist'] as $persistKey) { - if (array_key_exists($persistKey, $params) && !isset($url[$persistKey])) { - $url[$persistKey] = $params[$persistKey]; - } - } - return $url; - } - -/** - * Check if a URL array matches this route instance. - * - * If the URL matches the route parameters and settings, then - * return a generated string URL. If the URL doesn't match the route parameters, false will be returned. - * This method handles the reverse routing or conversion of URL arrays into string URLs. - * - * @param array $url An array of parameters to check matching with. - * @return mixed Either a string URL for the parameters if they match or false. - */ - public function match($url) { - if (!$this->compiled()) { - $this->compile(); - } - $defaults = $this->defaults; - - if (isset($defaults['prefix'])) { - $url['prefix'] = $defaults['prefix']; - } - - //check that all the key names are in the url - $keyNames = array_flip($this->keys); - if (array_intersect_key($keyNames, $url) !== $keyNames) { - return false; - } - - // Missing defaults is a fail. - if (array_diff_key($defaults, $url) !== array()) { - return false; - } - - $namedConfig = Router::namedConfig(); - $prefixes = Router::prefixes(); - $greedyNamed = $namedConfig['greedyNamed']; - $allowedNamedParams = $namedConfig['rules']; - - $named = $pass = array(); - - foreach ($url as $key => $value) { - // keys that exist in the defaults and have different values is a match failure. - $defaultExists = array_key_exists($key, $defaults); - if ($defaultExists && $defaults[$key] != $value) { - return false; - } elseif ($defaultExists) { - continue; - } - - // If the key is a routed key, its not different yet. - if (array_key_exists($key, $keyNames)) { - continue; - } - - // pull out passed args - $numeric = is_numeric($key); - if ($numeric && isset($defaults[$key]) && $defaults[$key] == $value) { - continue; - } elseif ($numeric) { - $pass[] = $value; - unset($url[$key]); - continue; - } - - // pull out named params if named params are greedy or a rule exists. - if (($greedyNamed || isset($allowedNamedParams[$key])) && - ($value !== false && $value !== null) && - (!in_array($key, $prefixes)) - ) { - $named[$key] = $value; - continue; - } - - // keys that don't exist are different. - if (!$defaultExists && !empty($value)) { - return false; - } - } - - //if a not a greedy route, no extra params are allowed. - if (!$this->_greedy && (!empty($pass) || !empty($named))) { - return false; - } - - //check patterns for routed params - if (!empty($this->options)) { - foreach ($this->options as $key => $pattern) { - if (array_key_exists($key, $url) && !preg_match('#^' . $pattern . '$#', $url[$key])) { - return false; - } - } - } - return $this->_writeUrl(array_merge($url, compact('pass', 'named'))); - } - -/** - * Converts a matching route array into a URL string. - * - * Composes the string URL using the template - * used to create the route. - * - * @param array $params The params to convert to a string URL. - * @return string Composed route string. - */ - protected function _writeUrl($params) { - if (isset($params['prefix'])) { - $prefixed = $params['prefix'] . '_'; - } - if (isset($prefixed, $params['action']) && strpos($params['action'], $prefixed) === 0) { - $params['action'] = substr($params['action'], strlen($prefixed)); - unset($params['prefix']); - } - - if (is_array($params['pass'])) { - $params['pass'] = implode('/', array_map('rawurlencode', $params['pass'])); - } - - $namedConfig = Router::namedConfig(); - $separator = $namedConfig['separator']; - - if (!empty($params['named']) && is_array($params['named'])) { - $named = array(); - foreach ($params['named'] as $key => $value) { - if (is_array($value)) { - $flat = Hash::flatten($value, '%5D%5B'); - foreach ($flat as $namedKey => $namedValue) { - $named[] = $key . "%5B{$namedKey}%5D" . $separator . rawurlencode($namedValue); - } - } else { - $named[] = $key . $separator . rawurlencode($value); - } - } - $params['pass'] = $params['pass'] . '/' . implode('/', $named); - } - $out = $this->template; - - if (!empty($this->keys)) { - $search = $replace = array(); - - foreach ($this->keys as $key) { - $string = null; - if (isset($params[$key])) { - $string = $params[$key]; - } elseif (strpos($out, $key) != strlen($out) - strlen($key)) { - $key .= '/'; - } - $search[] = ':' . $key; - $replace[] = $string; - } - $out = str_replace($search, $replace, $out); - } - - if (strpos($this->template, '**') !== false) { - $out = str_replace('**', $params['pass'], $out); - $out = str_replace('%2F', '/', $out); - } elseif (strpos($this->template, '*') !== false) { - $out = str_replace('*', $params['pass'], $out); - } - $out = str_replace('//', '/', $out); - return $out; - } - -/** - * Set state magic method to support var_export - * - * This method helps for applications that want to implement - * router caching. - * - * @param array $fields Key/Value of object attributes - * @return CakeRoute A new instance of the route - */ - public static function __set_state($fields) { - $class = function_exists('get_called_class') ? get_called_class() : __CLASS__; - $obj = new $class(''); - foreach ($fields as $field => $value) { - $obj->$field = $value; - } - return $obj; - } +class CakeRoute +{ + + /** + * An array of named segments in a Route. + * `/:controller/:action/:id` has 3 key elements + * + * @var array + */ + public $keys = []; + + /** + * An array of additional parameters for the Route. + * + * @var array + */ + public $options = []; + + /** + * Default parameters for a Route + * + * @var array + */ + public $defaults = []; + + /** + * The routes template string. + * + * @var string + */ + public $template = null; + + /** + * Is this route a greedy route? Greedy routes have a `/*` in their + * template + * + * @var string + */ + protected $_greedy = false; + + /** + * The compiled route regular expression + * + * @var string + */ + protected $_compiledRoute = null; + + /** + * HTTP header shortcut map. Used for evaluating header-based route expressions. + * + * @var array + */ + protected $_headerMap = [ + 'type' => 'content_type', + 'method' => 'request_method', + 'server' => 'server_name' + ]; + + /** + * Constructor for a Route + * + * @param string $template Template string with parameter placeholders + * @param array $defaults Array of defaults for the route. + * @param array $options Array of additional options for the Route + */ + public function __construct($template, $defaults = [], $options = []) + { + $this->template = $template; + $this->defaults = (array)$defaults; + $this->options = (array)$options; + } + + /** + * Set state magic method to support var_export + * + * This method helps for applications that want to implement + * router caching. + * + * @param array $fields Key/Value of object attributes + * @return CakeRoute A new instance of the route + */ + public static function __set_state($fields) + { + $class = function_exists('get_called_class') ? get_called_class() : __CLASS__; + $obj = new $class(''); + foreach ($fields as $field => $value) { + $obj->$field = $value; + } + return $obj; + } + + /** + * Checks to see if the given URL can be parsed by this route. + * + * If the route can be parsed an array of parameters will be returned; if not + * false will be returned. String URLs are parsed if they match a routes regular expression. + * + * @param string $url The URL to attempt to parse. + * @return mixed Boolean false on failure, otherwise an array or parameters + */ + public function parse($url) + { + if (!$this->compiled()) { + $this->compile(); + } + if (!preg_match($this->_compiledRoute, urldecode($url), $route)) { + return false; + } + foreach ($this->defaults as $key => $val) { + $key = (string)$key; + if ($key[0] === '[' && preg_match('/^\[(\w+)\]$/', $key, $header)) { + if (isset($this->_headerMap[$header[1]])) { + $header = $this->_headerMap[$header[1]]; + } else { + $header = 'http_' . $header[1]; + } + $header = strtoupper($header); + + $val = (array)$val; + $h = false; + + foreach ($val as $v) { + if (env($header) === $v) { + $h = true; + } + } + if (!$h) { + return false; + } + } + } + array_shift($route); + $count = count($this->keys); + for ($i = 0; $i <= $count; $i++) { + unset($route[$i]); + } + $route['pass'] = $route['named'] = []; + + // Assign defaults, set passed args to pass + foreach ($this->defaults as $key => $value) { + if (isset($route[$key])) { + continue; + } + if (is_int($key)) { + $route['pass'][] = $value; + continue; + } + $route[$key] = $value; + } + + if (isset($route['_args_'])) { + list($pass, $named) = $this->_parseArgs($route['_args_'], $route); + $route['pass'] = array_merge($route['pass'], $pass); + $route['named'] = $named; + unset($route['_args_']); + } + + if (isset($route['_trailing_'])) { + $route['pass'][] = $route['_trailing_']; + unset($route['_trailing_']); + } + + // restructure 'pass' key route params + if (isset($this->options['pass'])) { + $j = count($this->options['pass']); + while ($j--) { + if (isset($route[$this->options['pass'][$j]])) { + array_unshift($route['pass'], $route[$this->options['pass'][$j]]); + } + } + } + return $route; + } + + /** + * Check if a Route has been compiled into a regular expression. + * + * @return bool + */ + public function compiled() + { + return !empty($this->_compiledRoute); + } + + /** + * Compiles the route's regular expression. + * + * Modifies defaults property so all necessary keys are set + * and populates $this->names with the named routing elements. + * + * @return array Returns a string regular expression of the compiled route. + */ + public function compile() + { + if ($this->compiled()) { + return $this->_compiledRoute; + } + $this->_writeRoute(); + return $this->_compiledRoute; + } + + /** + * Builds a route regular expression. + * + * Uses the template, defaults and options properties to compile a + * regular expression that can be used to parse request strings. + * + * @return void + */ + protected function _writeRoute() + { + if (empty($this->template) || ($this->template === '/')) { + $this->_compiledRoute = '#^/*$#'; + $this->keys = []; + return; + } + $route = $this->template; + $names = $routeParams = []; + $parsed = preg_quote($this->template, '#'); + + preg_match_all('#:([A-Za-z0-9_-]+[A-Z0-9a-z])#', $route, $namedElements); + foreach ($namedElements[1] as $i => $name) { + $search = '\\' . $namedElements[0][$i]; + if (isset($this->options[$name])) { + $option = null; + if ($name !== 'plugin' && array_key_exists($name, $this->defaults)) { + $option = '?'; + } + $slashParam = '/\\' . $namedElements[0][$i]; + if (strpos($parsed, $slashParam) !== false) { + $routeParams[$slashParam] = '(?:/(?P<' . $name . '>' . $this->options[$name] . ')' . $option . ')' . $option; + } else { + $routeParams[$search] = '(?:(?P<' . $name . '>' . $this->options[$name] . ')' . $option . ')' . $option; + } + } else { + $routeParams[$search] = '(?:(?P<' . $name . '>[^/]+))'; + } + $names[] = $name; + } + if (preg_match('#\/\*\*$#', $route)) { + $parsed = preg_replace('#/\\\\\*\\\\\*$#', '(?:/(?P<_trailing_>.*))?', $parsed); + $this->_greedy = true; + } + if (preg_match('#\/\*$#', $route)) { + $parsed = preg_replace('#/\\\\\*$#', '(?:/(?P<_args_>.*))?', $parsed); + $this->_greedy = true; + } + krsort($routeParams); + $parsed = str_replace(array_keys($routeParams), array_values($routeParams), $parsed); + $this->_compiledRoute = '#^' . $parsed . '[/]*$#'; + $this->keys = $names; + + // Remove defaults that are also keys. They can cause match failures + foreach ($this->keys as $key) { + unset($this->defaults[$key]); + } + + $keys = $this->keys; + sort($keys); + $this->keys = array_reverse($keys); + } + + /** + * Parse passed and Named parameters into a list of passed args, and a hash of named parameters. + * The local and global configuration for named parameters will be used. + * + * @param string $args A string with the passed & named params. eg. /1/page:2 + * @param string $context The current route context, which should contain controller/action keys. + * @return array Array of ($pass, $named) + */ + protected function _parseArgs($args, $context) + { + $pass = $named = []; + $args = explode('/', $args); + + $namedConfig = Router::namedConfig(); + $greedy = $namedConfig['greedyNamed']; + $rules = $namedConfig['rules']; + if (!empty($this->options['named'])) { + $greedy = isset($this->options['greedyNamed']) && $this->options['greedyNamed'] === true; + foreach ((array)$this->options['named'] as $key => $val) { + if (is_numeric($key)) { + $rules[$val] = true; + continue; + } + $rules[$key] = $val; + } + } + + foreach ($args as $param) { + if (empty($param) && $param !== '0' && $param !== 0) { + continue; + } + + $separatorIsPresent = strpos($param, $namedConfig['separator']) !== false; + if ((!isset($this->options['named']) || !empty($this->options['named'])) && $separatorIsPresent) { + list($key, $val) = explode($namedConfig['separator'], $param, 2); + $hasRule = isset($rules[$key]); + $passIt = (!$hasRule && !$greedy) || ($hasRule && !$this->_matchNamed($val, $rules[$key], $context)); + if ($passIt) { + $pass[] = $param; + } else { + if (preg_match_all('/\[([A-Za-z0-9_-]+)?\]/', $key, $matches, PREG_SET_ORDER)) { + $matches = array_reverse($matches); + $parts = explode('[', $key); + $key = array_shift($parts); + $arr = $val; + foreach ($matches as $match) { + if (empty($match[1])) { + $arr = [$arr]; + } else { + $arr = [ + $match[1] => $arr + ]; + } + } + $val = $arr; + } + $named = array_merge_recursive($named, [$key => $val]); + } + } else { + $pass[] = $param; + } + } + return [$pass, $named]; + } + + /** + * Check if a named parameter matches the current rules. + * + * Return true if a given named $param's $val matches a given $rule depending on $context. + * Currently implemented rule types are controller, action and match that can be combined with each other. + * + * @param string $val The value of the named parameter + * @param array $rule The rule(s) to apply, can also be a match string + * @param string $context An array with additional context information (controller / action) + * @return bool + */ + protected function _matchNamed($val, $rule, $context) + { + if ($rule === true || $rule === false) { + return $rule; + } + if (is_string($rule)) { + $rule = ['match' => $rule]; + } + if (!is_array($rule)) { + return false; + } + + $controllerMatches = ( + !isset($rule['controller'], $context['controller']) || + in_array($context['controller'], (array)$rule['controller']) + ); + if (!$controllerMatches) { + return false; + } + $actionMatches = ( + !isset($rule['action'], $context['action']) || + in_array($context['action'], (array)$rule['action']) + ); + if (!$actionMatches) { + return false; + } + return (!isset($rule['match']) || preg_match('/' . $rule['match'] . '/', $val)); + } + + /** + * Apply persistent parameters to a URL array. Persistent parameters are a special + * key used during route creation to force route parameters to persist when omitted from + * a URL array. + * + * @param array $url The array to apply persistent parameters to. + * @param array $params An array of persistent values to replace persistent ones. + * @return array An array with persistent parameters applied. + */ + public function persistParams($url, $params) + { + if (empty($this->options['persist']) || !is_array($this->options['persist'])) { + return $url; + } + foreach ($this->options['persist'] as $persistKey) { + if (array_key_exists($persistKey, $params) && !isset($url[$persistKey])) { + $url[$persistKey] = $params[$persistKey]; + } + } + return $url; + } + + /** + * Check if a URL array matches this route instance. + * + * If the URL matches the route parameters and settings, then + * return a generated string URL. If the URL doesn't match the route parameters, false will be returned. + * This method handles the reverse routing or conversion of URL arrays into string URLs. + * + * @param array $url An array of parameters to check matching with. + * @return mixed Either a string URL for the parameters if they match or false. + */ + public function match($url) + { + if (!$this->compiled()) { + $this->compile(); + } + $defaults = $this->defaults; + + if (isset($defaults['prefix'])) { + $url['prefix'] = $defaults['prefix']; + } + + //check that all the key names are in the url + $keyNames = array_flip($this->keys); + if (array_intersect_key($keyNames, $url) !== $keyNames) { + return false; + } + + // Missing defaults is a fail. + if (array_diff_key($defaults, $url) !== []) { + return false; + } + + $namedConfig = Router::namedConfig(); + $prefixes = Router::prefixes(); + $greedyNamed = $namedConfig['greedyNamed']; + $allowedNamedParams = $namedConfig['rules']; + + $named = $pass = []; + + foreach ($url as $key => $value) { + // keys that exist in the defaults and have different values is a match failure. + $defaultExists = array_key_exists($key, $defaults); + if ($defaultExists && $defaults[$key] != $value) { + return false; + } else if ($defaultExists) { + continue; + } + + // If the key is a routed key, its not different yet. + if (array_key_exists($key, $keyNames)) { + continue; + } + + // pull out passed args + $numeric = is_numeric($key); + if ($numeric && isset($defaults[$key]) && $defaults[$key] == $value) { + continue; + } else if ($numeric) { + $pass[] = $value; + unset($url[$key]); + continue; + } + + // pull out named params if named params are greedy or a rule exists. + if (($greedyNamed || isset($allowedNamedParams[$key])) && + ($value !== false && $value !== null) && + (!in_array($key, $prefixes)) + ) { + $named[$key] = $value; + continue; + } + + // keys that don't exist are different. + if (!$defaultExists && !empty($value)) { + return false; + } + } + + //if a not a greedy route, no extra params are allowed. + if (!$this->_greedy && (!empty($pass) || !empty($named))) { + return false; + } + + //check patterns for routed params + if (!empty($this->options)) { + foreach ($this->options as $key => $pattern) { + if (array_key_exists($key, $url) && !preg_match('#^' . $pattern . '$#', $url[$key])) { + return false; + } + } + } + return $this->_writeUrl(array_merge($url, compact('pass', 'named'))); + } + + /** + * Converts a matching route array into a URL string. + * + * Composes the string URL using the template + * used to create the route. + * + * @param array $params The params to convert to a string URL. + * @return string Composed route string. + */ + protected function _writeUrl($params) + { + if (isset($params['prefix'])) { + $prefixed = $params['prefix'] . '_'; + } + if (isset($prefixed, $params['action']) && strpos($params['action'], $prefixed) === 0) { + $params['action'] = substr($params['action'], strlen($prefixed)); + unset($params['prefix']); + } + + if (is_array($params['pass'])) { + $params['pass'] = implode('/', array_map('rawurlencode', $params['pass'])); + } + + $namedConfig = Router::namedConfig(); + $separator = $namedConfig['separator']; + + if (!empty($params['named']) && is_array($params['named'])) { + $named = []; + foreach ($params['named'] as $key => $value) { + if (is_array($value)) { + $flat = Hash::flatten($value, '%5D%5B'); + foreach ($flat as $namedKey => $namedValue) { + $named[] = $key . "%5B{$namedKey}%5D" . $separator . rawurlencode($namedValue); + } + } else { + $named[] = $key . $separator . rawurlencode($value); + } + } + $params['pass'] = $params['pass'] . '/' . implode('/', $named); + } + $out = $this->template; + + if (!empty($this->keys)) { + $search = $replace = []; + + foreach ($this->keys as $key) { + $string = null; + if (isset($params[$key])) { + $string = $params[$key]; + } else if (strpos($out, $key) != strlen($out) - strlen($key)) { + $key .= '/'; + } + $search[] = ':' . $key; + $replace[] = $string; + } + $out = str_replace($search, $replace, $out); + } + + if (strpos($this->template, '**') !== false) { + $out = str_replace('**', $params['pass'], $out); + $out = str_replace('%2F', '/', $out); + } else if (strpos($this->template, '*') !== false) { + $out = str_replace('*', $params['pass'], $out); + } + $out = str_replace('//', '/', $out); + return $out; + } } diff --git a/lib/Cake/Routing/Route/PluginShortRoute.php b/lib/Cake/Routing/Route/PluginShortRoute.php index d504495e..cbf74741 100755 --- a/lib/Cake/Routing/Route/PluginShortRoute.php +++ b/lib/Cake/Routing/Route/PluginShortRoute.php @@ -21,39 +21,42 @@ * * @package Cake.Routing.Route */ -class PluginShortRoute extends CakeRoute { +class PluginShortRoute extends CakeRoute +{ -/** - * Parses a string URL into an array. If a plugin key is found, it will be copied to the - * controller parameter - * - * @param string $url The URL to parse - * @return mixed false on failure, or an array of request parameters - */ - public function parse($url) { - $params = parent::parse($url); - if (!$params) { - return false; - } - $params['controller'] = $params['plugin']; - return $params; - } + /** + * Parses a string URL into an array. If a plugin key is found, it will be copied to the + * controller parameter + * + * @param string $url The URL to parse + * @return mixed false on failure, or an array of request parameters + */ + public function parse($url) + { + $params = parent::parse($url); + if (!$params) { + return false; + } + $params['controller'] = $params['plugin']; + return $params; + } -/** - * Reverse route plugin shortcut URLs. If the plugin and controller - * are not the same the match is an auto fail. - * - * @param array $url Array of parameters to convert to a string. - * @return mixed either false or a string URL. - */ - public function match($url) { - if (isset($url['controller']) && isset($url['plugin']) && $url['plugin'] != $url['controller']) { - return false; - } - $this->defaults['controller'] = $url['controller']; - $result = parent::match($url); - unset($this->defaults['controller']); - return $result; - } + /** + * Reverse route plugin shortcut URLs. If the plugin and controller + * are not the same the match is an auto fail. + * + * @param array $url Array of parameters to convert to a string. + * @return mixed either false or a string URL. + */ + public function match($url) + { + if (isset($url['controller']) && isset($url['plugin']) && $url['plugin'] != $url['controller']) { + return false; + } + $this->defaults['controller'] = $url['controller']; + $result = parent::match($url); + unset($this->defaults['controller']); + return $result; + } } diff --git a/lib/Cake/Routing/Route/RedirectRoute.php b/lib/Cake/Routing/Route/RedirectRoute.php index 26c865fb..3af08117 100755 --- a/lib/Cake/Routing/Route/RedirectRoute.php +++ b/lib/Cake/Routing/Route/RedirectRoute.php @@ -24,102 +24,107 @@ * * @package Cake.Routing.Route */ -class RedirectRoute extends CakeRoute { +class RedirectRoute extends CakeRoute +{ -/** - * A CakeResponse object - * - * @var CakeResponse - */ - public $response = null; + /** + * A CakeResponse object + * + * @var CakeResponse + */ + public $response = null; -/** - * The location to redirect to. Either a string or a CakePHP array URL. - * - * @var mixed - */ - public $redirect; + /** + * The location to redirect to. Either a string or a CakePHP array URL. + * + * @var mixed + */ + public $redirect; -/** - * Flag for disabling exit() when this route parses a URL. - * - * @var bool - */ - public $stop = true; + /** + * Flag for disabling exit() when this route parses a URL. + * + * @var bool + */ + public $stop = true; -/** - * Constructor - * - * @param string $template Template string with parameter placeholders - * @param array $defaults Array of defaults for the route. - * @param array $options Array of additional options for the Route - */ - public function __construct($template, $defaults = array(), $options = array()) { - parent::__construct($template, $defaults, $options); - $this->redirect = (array)$defaults; - } + /** + * Constructor + * + * @param string $template Template string with parameter placeholders + * @param array $defaults Array of defaults for the route. + * @param array $options Array of additional options for the Route + */ + public function __construct($template, $defaults = [], $options = []) + { + parent::__construct($template, $defaults, $options); + $this->redirect = (array)$defaults; + } -/** - * Parses a string URL into an array. Parsed URLs will result in an automatic - * redirection - * - * @param string $url The URL to parse - * @return bool False on failure - */ - public function parse($url) { - $params = parent::parse($url); - if (!$params) { - return false; - } - if (!$this->response) { - $this->response = new CakeResponse(); - } - $redirect = $this->redirect; - if (count($this->redirect) === 1 && !isset($this->redirect['controller'])) { - $redirect = $this->redirect[0]; - } - if (isset($this->options['persist']) && is_array($redirect)) { - $redirect += array('named' => $params['named'], 'pass' => $params['pass'], 'url' => array()); - if (is_array($this->options['persist'])) { - foreach ($this->options['persist'] as $elem) { - if (isset($params[$elem])) { - $redirect[$elem] = $params[$elem]; - } - } - } - $redirect = Router::reverse($redirect); - } - $status = 301; - if (isset($this->options['status']) && ($this->options['status'] >= 300 && $this->options['status'] < 400)) { - $status = $this->options['status']; - } - $this->response->header(array('Location' => Router::url($redirect, true))); - $this->response->statusCode($status); - $this->response->send(); - $this->_stop(); - } + /** + * Parses a string URL into an array. Parsed URLs will result in an automatic + * redirection + * + * @param string $url The URL to parse + * @return bool False on failure + */ + public function parse($url) + { + $params = parent::parse($url); + if (!$params) { + return false; + } + if (!$this->response) { + $this->response = new CakeResponse(); + } + $redirect = $this->redirect; + if (count($this->redirect) === 1 && !isset($this->redirect['controller'])) { + $redirect = $this->redirect[0]; + } + if (isset($this->options['persist']) && is_array($redirect)) { + $redirect += ['named' => $params['named'], 'pass' => $params['pass'], 'url' => []]; + if (is_array($this->options['persist'])) { + foreach ($this->options['persist'] as $elem) { + if (isset($params[$elem])) { + $redirect[$elem] = $params[$elem]; + } + } + } + $redirect = Router::reverse($redirect); + } + $status = 301; + if (isset($this->options['status']) && ($this->options['status'] >= 300 && $this->options['status'] < 400)) { + $status = $this->options['status']; + } + $this->response->header(['Location' => Router::url($redirect, true)]); + $this->response->statusCode($status); + $this->response->send(); + $this->_stop(); + } -/** - * There is no reverse routing redirection routes - * - * @param array $url Array of parameters to convert to a string. - * @return mixed either false or a string URL. - */ - public function match($url) { - return false; - } + /** + * Stop execution of the current script. Wraps exit() making + * testing easier. + * + * @param int|string $code See http://php.net/exit for values + * @return void + */ + protected function _stop($code = 0) + { + if ($this->stop) { + exit($code); + } + } -/** - * Stop execution of the current script. Wraps exit() making - * testing easier. - * - * @param int|string $code See http://php.net/exit for values - * @return void - */ - protected function _stop($code = 0) { - if ($this->stop) { - exit($code); - } - } + /** + * There is no reverse routing redirection routes + * + * @param array $url Array of parameters to convert to a string. + * @return mixed either false or a string URL. + */ + public function match($url) + { + return false; + } } diff --git a/lib/Cake/Routing/Router.php b/lib/Cake/Routing/Router.php index 368a2cef..4c4a2931 100755 --- a/lib/Cake/Routing/Router.php +++ b/lib/Cake/Routing/Router.php @@ -38,1251 +38,1275 @@ * * @package Cake.Routing */ -class Router { - -/** - * Array of routes connected with Router::connect() - * - * @var array - */ - public static $routes = array(); - -/** - * Have routes been loaded - * - * @var bool - */ - public static $initialized = false; - -/** - * Contains the base string that will be applied to all generated URLs - * For example `https://example.com` - * - * @var string - */ - protected static $_fullBaseUrl; - -/** - * List of action prefixes used in connected routes. - * Includes admin prefix - * - * @var array - */ - protected static $_prefixes = array(); - -/** - * Directive for Router to parse out file extensions for mapping to Content-types. - * - * @var bool - */ - protected static $_parseExtensions = false; - -/** - * List of valid extensions to parse from a URL. If null, any extension is allowed. - * - * @var array - */ - protected static $_validExtensions = array(); - -/** - * Regular expression for action names - * - * @var string - */ - const ACTION = 'index|show|add|create|edit|update|remove|del|delete|view|item'; - -/** - * Regular expression for years - * - * @var string - */ - const YEAR = '[12][0-9]{3}'; - -/** - * Regular expression for months - * - * @var string - */ - const MONTH = '0[1-9]|1[012]'; - -/** - * Regular expression for days - * - * @var string - */ - const DAY = '0[1-9]|[12][0-9]|3[01]'; - -/** - * Regular expression for auto increment IDs - * - * @var string - */ - const ID = '[0-9]+'; - -/** - * Regular expression for UUIDs - * - * @var string - */ - const UUID = '[A-Fa-f0-9]{8}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{12}'; - -/** - * Named expressions - * - * @var array - */ - protected static $_namedExpressions = array( - 'Action' => Router::ACTION, - 'Year' => Router::YEAR, - 'Month' => Router::MONTH, - 'Day' => Router::DAY, - 'ID' => Router::ID, - 'UUID' => Router::UUID - ); - -/** - * Stores all information necessary to decide what named arguments are parsed under what conditions. - * - * @var string - */ - protected static $_namedConfig = array( - 'default' => array('page', 'fields', 'order', 'limit', 'recursive', 'sort', 'direction', 'step'), - 'greedyNamed' => true, - 'separator' => ':', - 'rules' => false, - ); - -/** - * The route matching the URL of the current request - * - * @var array - */ - protected static $_currentRoute = array(); - -/** - * Default HTTP request method => controller action map. - * - * @var array - */ - protected static $_resourceMap = array( - array('action' => 'index', 'method' => 'GET', 'id' => false), - array('action' => 'view', 'method' => 'GET', 'id' => true), - array('action' => 'add', 'method' => 'POST', 'id' => false), - array('action' => 'edit', 'method' => 'PUT', 'id' => true), - array('action' => 'delete', 'method' => 'DELETE', 'id' => true), - array('action' => 'edit', 'method' => 'POST', 'id' => true) - ); - -/** - * List of resource-mapped controllers - * - * @var array - */ - protected static $_resourceMapped = array(); - -/** - * Maintains the request object stack for the current request. - * This will contain more than one request object when requestAction is used. - * - * @var array - */ - protected static $_requests = array(); - -/** - * Initial state is populated the first time reload() is called which is at the bottom - * of this file. This is a cheat as get_class_vars() returns the value of static vars even if they - * have changed. - * - * @var array - */ - protected static $_initialState = array(); - -/** - * Default route class to use - * - * @var string - */ - protected static $_routeClass = 'CakeRoute'; - -/** - * Set the default route class to use or return the current one - * - * @param string $routeClass The route class to set as default. - * @return string|null The default route class. - * @throws RouterException - */ - public static function defaultRouteClass($routeClass = null) { - if ($routeClass === null) { - return static::$_routeClass; - } - - static::$_routeClass = static::_validateRouteClass($routeClass); - } - -/** - * Validates that the passed route class exists and is a subclass of CakeRoute - * - * @param string $routeClass Route class name - * @return string - * @throws RouterException - */ - protected static function _validateRouteClass($routeClass) { - if ($routeClass !== 'CakeRoute' && - (!class_exists($routeClass) || !is_subclass_of($routeClass, 'CakeRoute')) - ) { - throw new RouterException(__d('cake_dev', 'Route class not found, or route class is not a subclass of CakeRoute')); - } - return $routeClass; - } - -/** - * Sets the Routing prefixes. - * - * @return void - */ - protected static function _setPrefixes() { - $routing = Configure::read('Routing'); - if (!empty($routing['prefixes'])) { - static::$_prefixes = array_merge(static::$_prefixes, (array)$routing['prefixes']); - } - } - -/** - * Gets the named route elements for use in app/Config/routes.php - * - * @return array Named route elements - * @see Router::$_namedExpressions - */ - public static function getNamedExpressions() { - return static::$_namedExpressions; - } - -/** - * Resource map getter & setter. - * - * @param array $resourceMap Resource map - * @return mixed - * @see Router::$_resourceMap - */ - public static function resourceMap($resourceMap = null) { - if ($resourceMap === null) { - return static::$_resourceMap; - } - static::$_resourceMap = $resourceMap; - } - -/** - * Connects a new Route in the router. - * - * Routes are a way of connecting request URLs to objects in your application. At their core routes - * are a set of regular expressions that are used to match requests to destinations. - * - * Examples: - * - * `Router::connect('/:controller/:action/*');` - * - * The first token ':controller' will be used as a controller name while the second is used as the action name. - * the '/*' syntax makes this route greedy in that it will match requests like `/posts/index` as well as requests - * like `/posts/edit/1/foo/bar`. - * - * `Router::connect('/home-page', array('controller' => 'pages', 'action' => 'display', 'home'));` - * - * The above shows the use of route parameter defaults, and providing routing parameters for a static route. - * - * ``` - * Router::connect( - * '/:lang/:controller/:action/:id', - * array(), - * array('id' => '[0-9]+', 'lang' => '[a-z]{3}') - * ); - * ``` - * - * Shows connecting a route with custom route parameters as well as providing patterns for those parameters. - * Patterns for routing parameters do not need capturing groups, as one will be added for each route params. - * - * $defaults is merged with the results of parsing the request URL to form the final routing destination and its - * parameters. This destination is expressed as an associative array by Router. See the output of {@link parse()}. - * - * $options offers four 'special' keys. `pass`, `named`, `persist` and `routeClass` - * have special meaning in the $options array. - * - * - `pass` is used to define which of the routed parameters should be shifted into the pass array. Adding a - * parameter to pass will remove it from the regular route array. Ex. `'pass' => array('slug')` - * - `persist` is used to define which route parameters should be automatically included when generating - * new URLs. You can override persistent parameters by redefining them in a URL or remove them by - * setting the parameter to `false`. Ex. `'persist' => array('lang')` - * - `routeClass` is used to extend and change how individual routes parse requests and handle reverse routing, - * via a custom routing class. Ex. `'routeClass' => 'SlugRoute'` - * - `named` is used to configure named parameters at the route level. This key uses the same options - * as Router::connectNamed() - * - * You can also add additional conditions for matching routes to the $defaults array. - * The following conditions can be used: - * - * - `[type]` Only match requests for specific content types. - * - `[method]` Only match requests with specific HTTP verbs. - * - `[server]` Only match when $_SERVER['SERVER_NAME'] matches the given value. - * - * Example of using the `[method]` condition: - * - * `Router::connect('/tasks', array('controller' => 'tasks', 'action' => 'index', '[method]' => 'GET'));` - * - * The above route will only be matched for GET requests. POST requests will fail to match this route. - * - * @param string $route A string describing the template of the route - * @param array $defaults An array describing the default route parameters. These parameters will be used by default - * and can supply routing parameters that are not dynamic. See above. - * @param array $options An array matching the named elements in the route to regular expressions which that - * element should match. Also contains additional parameters such as which routed parameters should be - * shifted into the passed arguments, supplying patterns for routing parameters and supplying the name of a - * custom routing class. - * @see routes - * @see parse(). - * @return array Array of routes - * @throws RouterException - */ - public static function connect($route, $defaults = array(), $options = array()) { - static::$initialized = true; - - foreach (static::$_prefixes as $prefix) { - if (isset($defaults[$prefix])) { - if ($defaults[$prefix]) { - $defaults['prefix'] = $prefix; - } else { - unset($defaults[$prefix]); - } - break; - } - } - if (isset($defaults['prefix']) && !in_array($defaults['prefix'], static::$_prefixes)) { - static::$_prefixes[] = $defaults['prefix']; - } - $defaults += array('plugin' => null); - if (empty($options['action'])) { - $defaults += array('action' => 'index'); - } - $routeClass = static::$_routeClass; - if (isset($options['routeClass'])) { - if (strpos($options['routeClass'], '.') === false) { - $routeClass = $options['routeClass']; - } else { - list(, $routeClass) = pluginSplit($options['routeClass'], true); - } - $routeClass = static::_validateRouteClass($routeClass); - unset($options['routeClass']); - } - if ($routeClass === 'RedirectRoute' && isset($defaults['redirect'])) { - $defaults = $defaults['redirect']; - } - static::$routes[] = new $routeClass($route, $defaults, $options); - return static::$routes; - } - -/** - * Connects a new redirection Route in the router. - * - * Redirection routes are different from normal routes as they perform an actual - * header redirection if a match is found. The redirection can occur within your - * application or redirect to an outside location. - * - * Examples: - * - * `Router::redirect('/home/*', array('controller' => 'posts', 'action' => 'view'), array('persist' => true));` - * - * Redirects /home/* to /posts/view and passes the parameters to /posts/view. Using an array as the - * redirect destination allows you to use other routes to define where a URL string should be redirected to. - * - * `Router::redirect('/posts/*', 'http://google.com', array('status' => 302));` - * - * Redirects /posts/* to http://google.com with a HTTP status of 302 - * - * ### Options: - * - * - `status` Sets the HTTP status (default 301) - * - `persist` Passes the params to the redirected route, if it can. This is useful with greedy routes, - * routes that end in `*` are greedy. As you can remap URLs and not loose any passed/named args. - * - * @param string $route A string describing the template of the route - * @param array $url A URL to redirect to. Can be a string or a CakePHP array-based URL - * @param array $options An array matching the named elements in the route to regular expressions which that - * element should match. Also contains additional parameters such as which routed parameters should be - * shifted into the passed arguments. As well as supplying patterns for routing parameters. - * @see routes - * @return array Array of routes - */ - public static function redirect($route, $url, $options = array()) { - App::uses('RedirectRoute', 'Routing/Route'); - $options['routeClass'] = 'RedirectRoute'; - if (is_string($url)) { - $url = array('redirect' => $url); - } - return static::connect($route, $url, $options); - } - -/** - * Specifies what named parameters CakePHP should be parsing out of incoming URLs. By default - * CakePHP will parse every named parameter out of incoming URLs. However, if you want to take more - * control over how named parameters are parsed you can use one of the following setups: - * - * Do not parse any named parameters: - * - * ``` Router::connectNamed(false); ``` - * - * Parse only default parameters used for CakePHP's pagination: - * - * ``` Router::connectNamed(false, array('default' => true)); ``` - * - * Parse only the page parameter if its value is a number: - * - * ``` Router::connectNamed(array('page' => '[\d]+'), array('default' => false, 'greedy' => false)); ``` - * - * Parse only the page parameter no matter what. - * - * ``` Router::connectNamed(array('page'), array('default' => false, 'greedy' => false)); ``` - * - * Parse only the page parameter if the current action is 'index'. - * - * ``` - * Router::connectNamed( - * array('page' => array('action' => 'index')), - * array('default' => false, 'greedy' => false) - * ); - * ``` - * - * Parse only the page parameter if the current action is 'index' and the controller is 'pages'. - * - * ``` - * Router::connectNamed( - * array('page' => array('action' => 'index', 'controller' => 'pages')), - * array('default' => false, 'greedy' => false) - * ); - * ``` - * - * ### Options - * - * - `greedy` Setting this to true will make Router parse all named params. Setting it to false will - * parse only the connected named params. - * - `default` Set this to true to merge in the default set of named parameters. - * - `reset` Set to true to clear existing rules and start fresh. - * - `separator` Change the string used to separate the key & value in a named parameter. Defaults to `:` - * - * @param array $named A list of named parameters. Key value pairs are accepted where values are - * either regex strings to match, or arrays as seen above. - * @param array $options Allows to control all settings: separator, greedy, reset, default - * @return array - */ - public static function connectNamed($named, $options = array()) { - if (isset($options['separator'])) { - static::$_namedConfig['separator'] = $options['separator']; - unset($options['separator']); - } - - if ($named === true || $named === false) { - $options += array('default' => $named, 'reset' => true, 'greedy' => $named); - $named = array(); - } else { - $options += array('default' => false, 'reset' => false, 'greedy' => true); - } - - if ($options['reset'] || static::$_namedConfig['rules'] === false) { - static::$_namedConfig['rules'] = array(); - } - - if ($options['default']) { - $named = array_merge($named, static::$_namedConfig['default']); - } - - foreach ($named as $key => $val) { - if (is_numeric($key)) { - static::$_namedConfig['rules'][$val] = true; - } else { - static::$_namedConfig['rules'][$key] = $val; - } - } - static::$_namedConfig['greedyNamed'] = $options['greedy']; - return static::$_namedConfig; - } - -/** - * Gets the current named parameter configuration values. - * - * @return array - * @see Router::$_namedConfig - */ - public static function namedConfig() { - return static::$_namedConfig; - } - -/** - * Creates REST resource routes for the given controller(s). When creating resource routes - * for a plugin, by default the prefix will be changed to the lower_underscore version of the plugin - * name. By providing a prefix you can override this behavior. - * - * ### Options: - * - * - 'id' - The regular expression fragment to use when matching IDs. By default, matches - * integer values and UUIDs. - * - 'prefix' - URL prefix to use for the generated routes. Defaults to '/'. - * - 'connectOptions' – Custom options for connecting the routes. - * - * @param string|array $controller A controller name or array of controller names (i.e. "Posts" or "ListItems") - * @param array $options Options to use when generating REST routes - * @return array Array of mapped resources - */ - public static function mapResources($controller, $options = array()) { - $hasPrefix = isset($options['prefix']); - $options += array( - 'connectOptions' => array(), - 'prefix' => '/', - 'id' => static::ID . '|' . static::UUID - ); - - $prefix = $options['prefix']; - $connectOptions = $options['connectOptions']; - unset($options['connectOptions']); - if (strpos($prefix, '/') !== 0) { - $prefix = '/' . $prefix; - } - if (substr($prefix, -1) !== '/') { - $prefix .= '/'; - } - - foreach ((array)$controller as $name) { - list($plugin, $name) = pluginSplit($name); - $urlName = Inflector::underscore($name); - $plugin = Inflector::underscore($plugin); - if ($plugin && !$hasPrefix) { - $prefix = '/' . $plugin . '/'; - } - - foreach (static::$_resourceMap as $params) { - $url = $prefix . $urlName . (($params['id']) ? '/:id' : ''); - - Router::connect($url, - array( - 'plugin' => $plugin, - 'controller' => $urlName, - 'action' => $params['action'], - '[method]' => $params['method'] - ), - array_merge( - array('id' => $options['id'], 'pass' => array('id')), - $connectOptions - ) - ); - } - static::$_resourceMapped[] = $urlName; - } - return static::$_resourceMapped; - } - -/** - * Returns the list of prefixes used in connected routes - * - * @return array A list of prefixes used in connected routes - */ - public static function prefixes() { - return static::$_prefixes; - } - -/** - * Parses given URL string. Returns 'routing' parameters for that URL. - * - * @param string $url URL to be parsed - * @return array Parsed elements from URL - */ - public static function parse($url) { - if (!static::$initialized) { - static::_loadRoutes(); - } - - $ext = null; - $out = array(); - - if (strlen($url) && strpos($url, '/') !== 0) { - $url = '/' . $url; - } - if (strpos($url, '?') !== false) { - list($url, $queryParameters) = explode('?', $url, 2); - parse_str($queryParameters, $queryParameters); - } - - extract(static::_parseExtension($url)); - - foreach (static::$routes as $route) { - if (($r = $route->parse($url)) !== false) { - static::$_currentRoute[] = $route; - $out = $r; - break; - } - } - if (isset($out['prefix'])) { - $out['action'] = $out['prefix'] . '_' . $out['action']; - } - - if (!empty($ext) && !isset($out['ext'])) { - $out['ext'] = $ext; - } - - if (!empty($queryParameters) && !isset($out['?'])) { - $out['?'] = $queryParameters; - } - return $out; - } - -/** - * Parses a file extension out of a URL, if Router::parseExtensions() is enabled. - * - * @param string $url URL. - * @return array Returns an array containing the altered URL and the parsed extension. - */ - protected static function _parseExtension($url) { - $ext = null; - - if (static::$_parseExtensions) { - if (preg_match('/\.[0-9a-zA-Z]*$/', $url, $match) === 1) { - $match = substr($match[0], 1); - if (empty(static::$_validExtensions)) { - $url = substr($url, 0, strpos($url, '.' . $match)); - $ext = $match; - } else { - foreach (static::$_validExtensions as $name) { - if (strcasecmp($name, $match) === 0) { - $url = substr($url, 0, strpos($url, '.' . $name)); - $ext = $match; - break; - } - } - } - } - } - return compact('ext', 'url'); - } - -/** - * Takes parameter and path information back from the Dispatcher, sets these - * parameters as the current request parameters that are merged with URL arrays - * created later in the request. - * - * Nested requests will create a stack of requests. You can remove requests using - * Router::popRequest(). This is done automatically when using CakeObject::requestAction(). - * - * Will accept either a CakeRequest object or an array of arrays. Support for - * accepting arrays may be removed in the future. - * - * @param CakeRequest|array $request Parameters and path information or a CakeRequest object. - * @return void - */ - public static function setRequestInfo($request) { - if ($request instanceof CakeRequest) { - static::$_requests[] = $request; - } else { - $requestObj = new CakeRequest(); - $request += array(array(), array()); - $request[0] += array('controller' => false, 'action' => false, 'plugin' => null); - $requestObj->addParams($request[0])->addPaths($request[1]); - static::$_requests[] = $requestObj; - } - } - -/** - * Pops a request off of the request stack. Used when doing requestAction - * - * @return CakeRequest The request removed from the stack. - * @see Router::setRequestInfo() - * @see Object::requestAction() - */ - public static function popRequest() { - return array_pop(static::$_requests); - } - -/** - * Gets the current request object, or the first one. - * - * @param bool $current True to get the current request object, or false to get the first one. - * @return CakeRequest|null Null if stack is empty. - */ - public static function getRequest($current = false) { - if ($current) { - $i = count(static::$_requests) - 1; - return isset(static::$_requests[$i]) ? static::$_requests[$i] : null; - } - return isset(static::$_requests[0]) ? static::$_requests[0] : null; - } - -/** - * Gets parameter information - * - * @param bool $current Get current request parameter, useful when using requestAction - * @return array Parameter information - */ - public static function getParams($current = false) { - if ($current && static::$_requests) { - return static::$_requests[count(static::$_requests) - 1]->params; - } - if (isset(static::$_requests[0])) { - return static::$_requests[0]->params; - } - return array(); - } - -/** - * Gets URL parameter by name - * - * @param string $name Parameter name - * @param bool $current Current parameter, useful when using requestAction - * @return string|null Parameter value - */ - public static function getParam($name = 'controller', $current = false) { - $params = Router::getParams($current); - if (isset($params[$name])) { - return $params[$name]; - } - return null; - } - -/** - * Gets path information - * - * @param bool $current Current parameter, useful when using requestAction - * @return array - */ - public static function getPaths($current = false) { - if ($current) { - return static::$_requests[count(static::$_requests) - 1]; - } - if (!isset(static::$_requests[0])) { - return array('base' => null); - } - return array('base' => static::$_requests[0]->base); - } - -/** - * Reloads default Router settings. Resets all class variables and - * removes all connected routes. - * - * @return void - */ - public static function reload() { - if (empty(static::$_initialState)) { - static::$_initialState = get_class_vars('Router'); - static::_setPrefixes(); - return; - } - foreach (static::$_initialState as $key => $val) { - if ($key !== '_initialState') { - static::${$key} = $val; - } - } - static::_setPrefixes(); - } - -/** - * Promote a route (by default, the last one added) to the beginning of the list - * - * @param int $which A zero-based array index representing the route to move. For example, - * if 3 routes have been added, the last route would be 2. - * @return bool Returns false if no route exists at the position specified by $which. - */ - public static function promote($which = null) { - if ($which === null) { - $which = count(static::$routes) - 1; - } - if (!isset(static::$routes[$which])) { - return false; - } - $route =& static::$routes[$which]; - unset(static::$routes[$which]); - array_unshift(static::$routes, $route); - return true; - } - -/** - * Finds URL for specified action. - * - * Returns a URL pointing to a combination of controller and action. Param - * $url can be: - * - * - Empty - the method will find address to actual controller/action. - * - '/' - the method will find base URL of application. - * - A combination of controller/action - the method will find URL for it. - * - * There are a few 'special' parameters that can change the final URL string that is generated - * - * - `base` - Set to false to remove the base path from the generated URL. If your application - * is not in the root directory, this can be used to generate URLs that are 'cake relative'. - * cake relative URLs are required when using requestAction. - * - `?` - Takes an array of query string parameters - * - `#` - Allows you to set URL hash fragments. - * - `full_base` - If true the `Router::fullBaseUrl()` value will be prepended to generated URLs. - * - * @param string|array $url Cake-relative URL, like "/products/edit/92" or "/presidents/elect/4" - * or an array specifying any of the following: 'controller', 'action', - * and/or 'plugin', in addition to named arguments (keyed array elements), - * and standard URL arguments (indexed array elements) - * @param bool|array $full If (bool) true, the full base URL will be prepended to the result. - * If an array accepts the following keys - * - escape - used when making URLs embedded in html escapes query string '&' - * - full - if true the full base URL will be prepended. - * @return string Full translated URL with base path. - */ - public static function url($url = null, $full = false) { - if (!static::$initialized) { - static::_loadRoutes(); - } - - $params = array('plugin' => null, 'controller' => null, 'action' => 'index'); - - if (is_bool($full)) { - $escape = false; - } else { - extract($full + array('escape' => false, 'full' => false)); - } - - $path = array('base' => null); - if (!empty(static::$_requests)) { - $request = static::$_requests[count(static::$_requests) - 1]; - $params = $request->params; - $path = array('base' => $request->base, 'here' => $request->here); - } - if (empty($path['base'])) { - $path['base'] = Configure::read('App.base'); - } - - $base = $path['base']; - $extension = $output = $q = $frag = null; - - if (empty($url)) { - $output = isset($path['here']) ? $path['here'] : '/'; - if ($full) { - $output = static::fullBaseUrl() . $output; - } - return $output; - } elseif (is_array($url)) { - if (isset($url['base']) && $url['base'] === false) { - $base = null; - unset($url['base']); - } - if (isset($url['full_base']) && $url['full_base'] === true) { - $full = true; - unset($url['full_base']); - } - if (isset($url['?'])) { - $q = $url['?']; - unset($url['?']); - } - if (isset($url['#'])) { - $frag = '#' . $url['#']; - unset($url['#']); - } - if (isset($url['ext'])) { - $extension = '.' . $url['ext']; - unset($url['ext']); - } - if (empty($url['action'])) { - if (empty($url['controller']) || $params['controller'] === $url['controller']) { - $url['action'] = $params['action']; - } else { - $url['action'] = 'index'; - } - } - - $prefixExists = (array_intersect_key($url, array_flip(static::$_prefixes))); - foreach (static::$_prefixes as $prefix) { - if (!empty($params[$prefix]) && !$prefixExists) { - $url[$prefix] = true; - } elseif (isset($url[$prefix]) && !$url[$prefix]) { - unset($url[$prefix]); - } - if (isset($url[$prefix]) && strpos($url['action'], $prefix . '_') === 0) { - $url['action'] = substr($url['action'], strlen($prefix) + 1); - } - } - - $url += array('controller' => $params['controller'], 'plugin' => $params['plugin']); - - $match = false; - - foreach (static::$routes as $route) { - $originalUrl = $url; - - $url = $route->persistParams($url, $params); - - if ($match = $route->match($url)) { - $output = trim($match, '/'); - break; - } - $url = $originalUrl; - } - if ($match === false) { - $output = static::_handleNoRoute($url); - } - } else { - if (preg_match('/^([a-z][a-z0-9.+\-]+:|:?\/\/|[#?])/i', $url)) { - return $url; - } - if (substr($url, 0, 1) === '/') { - $output = substr($url, 1); - } else { - foreach (static::$_prefixes as $prefix) { - if (isset($params[$prefix])) { - $output .= $prefix . '/'; - break; - } - } - if (!empty($params['plugin']) && $params['plugin'] !== $params['controller']) { - $output .= Inflector::underscore($params['plugin']) . '/'; - } - $output .= Inflector::underscore($params['controller']) . '/' . $url; - } - } - $protocol = preg_match('#^[a-z][a-z0-9+\-.]*\://#i', $output); - if ($protocol === 0) { - $output = str_replace('//', '/', $base . '/' . $output); - - if ($full) { - $output = static::fullBaseUrl() . $output; - } - if (!empty($extension)) { - $output = rtrim($output, '/'); - } - } - return $output . $extension . static::queryString($q, array(), $escape) . $frag; - } - -/** - * Sets the full base URL that will be used as a prefix for generating - * fully qualified URLs for this application. If no parameters are passed, - * the currently configured value is returned. - * - * ## Note: - * - * If you change the configuration value ``App.fullBaseUrl`` during runtime - * and expect the router to produce links using the new setting, you are - * required to call this method passing such value again. - * - * @param string $base the prefix for URLs generated containing the domain. - * For example: ``http://example.com`` - * @return string - */ - public static function fullBaseUrl($base = null) { - if ($base !== null) { - static::$_fullBaseUrl = $base; - Configure::write('App.fullBaseUrl', $base); - } - if (empty(static::$_fullBaseUrl)) { - static::$_fullBaseUrl = Configure::read('App.fullBaseUrl'); - } - return static::$_fullBaseUrl; - } - -/** - * A special fallback method that handles URL arrays that cannot match - * any defined routes. - * - * @param array $url A URL that didn't match any routes - * @return string A generated URL for the array - * @see Router::url() - */ - protected static function _handleNoRoute($url) { - $named = $args = array(); - $skip = array_merge( - array('bare', 'action', 'controller', 'plugin', 'prefix'), - static::$_prefixes - ); - - $keys = array_values(array_diff(array_keys($url), $skip)); - - // Remove this once parsed URL parameters can be inserted into 'pass' - foreach ($keys as $key) { - if (is_numeric($key)) { - $args[] = $url[$key]; - } else { - $named[$key] = $url[$key]; - } - } - - list($args, $named) = array(Hash::filter($args), Hash::filter($named)); - foreach (static::$_prefixes as $prefix) { - $prefixed = $prefix . '_'; - if (!empty($url[$prefix]) && strpos($url['action'], $prefixed) === 0) { - $url['action'] = substr($url['action'], strlen($prefixed) * -1); - break; - } - } - - if (empty($named) && empty($args) && (!isset($url['action']) || $url['action'] === 'index')) { - $url['action'] = null; - } - - $urlOut = array_filter(array($url['controller'], $url['action'])); - - if (isset($url['plugin'])) { - array_unshift($urlOut, $url['plugin']); - } - - foreach (static::$_prefixes as $prefix) { - if (isset($url[$prefix])) { - array_unshift($urlOut, $prefix); - break; - } - } - $output = implode('/', $urlOut); - - if (!empty($args)) { - $output .= '/' . implode('/', array_map('rawurlencode', $args)); - } - - if (!empty($named)) { - foreach ($named as $name => $value) { - if (is_array($value)) { - $flattend = Hash::flatten($value, '%5D%5B'); - foreach ($flattend as $namedKey => $namedValue) { - $output .= '/' . $name . "%5B{$namedKey}%5D" . static::$_namedConfig['separator'] . rawurlencode($namedValue); - } - } else { - $output .= '/' . $name . static::$_namedConfig['separator'] . rawurlencode($value); - } - } - } - return $output; - } - -/** - * Generates a well-formed querystring from $q - * - * @param string|array $q Query string Either a string of already compiled query string arguments or - * an array of arguments to convert into a query string. - * @param array $extra Extra querystring parameters. - * @param bool $escape Whether or not to use escaped & - * @return string|null - */ - public static function queryString($q, $extra = array(), $escape = false) { - if (empty($q) && empty($extra)) { - return null; - } - $join = '&'; - if ($escape === true) { - $join = '&'; - } - $out = ''; - - if (is_array($q)) { - $q = array_merge($q, $extra); - } else { - $out = $q; - $q = $extra; - } - $addition = http_build_query($q, null, $join); - - if ($out && $addition && substr($out, strlen($join) * -1, strlen($join)) !== $join) { - $out .= $join; - } - - $out .= $addition; - - if (isset($out[0]) && $out[0] !== '?') { - $out = '?' . $out; - } - return $out; - } - -/** - * Reverses a parsed parameter array into an array. - * - * Works similarly to Router::url(), but since parsed URL's contain additional - * 'pass' and 'named' as well as 'url.url' keys. Those keys need to be specially - * handled in order to reverse a params array into a string URL. - * - * This will strip out 'autoRender', 'bare', 'requested', and 'return' param names as those - * are used for CakePHP internals and should not normally be part of an output URL. - * - * @param CakeRequest|array $params The params array or CakeRequest object that needs to be reversed. - * @return array The URL array ready to be used for redirect or HTML link. - */ - public static function reverseToArray($params) { - if ($params instanceof CakeRequest) { - $url = $params->query; - $params = $params->params; - } else { - $url = $params['url']; - } - $pass = isset($params['pass']) ? $params['pass'] : array(); - $named = isset($params['named']) ? $params['named'] : array(); - unset( - $params['pass'], $params['named'], $params['paging'], $params['models'], $params['url'], $url['url'], - $params['autoRender'], $params['bare'], $params['requested'], $params['return'], - $params['_Token'] - ); - $params = array_merge($params, $pass, $named); - if (!empty($url)) { - $params['?'] = $url; - } - return $params; - } - -/** - * Reverses a parsed parameter array into a string. - * - * Works similarly to Router::url(), but since parsed URL's contain additional - * 'pass' and 'named' as well as 'url.url' keys. Those keys need to be specially - * handled in order to reverse a params array into a string URL. - * - * This will strip out 'autoRender', 'bare', 'requested', and 'return' param names as those - * are used for CakePHP internals and should not normally be part of an output URL. - * - * @param CakeRequest|array $params The params array or CakeRequest object that needs to be reversed. - * @param bool $full Set to true to include the full URL including the protocol when reversing - * the URL. - * @return string The string that is the reversed result of the array - */ - public static function reverse($params, $full = false) { - $params = Router::reverseToArray($params, $full); - return Router::url($params, $full); - } - -/** - * Normalizes a URL for purposes of comparison. - * - * Will strip the base path off and replace any double /'s. - * It will not unify the casing and underscoring of the input value. - * - * @param array|string $url URL to normalize Either an array or a string URL. - * @return string Normalized URL - */ - public static function normalize($url = '/') { - if (is_array($url)) { - $url = Router::url($url); - } - if (preg_match('/^[a-z\-]+:\/\//', $url)) { - return $url; - } - $request = Router::getRequest(); - - if (!empty($request->base) && stristr($url, $request->base)) { - $url = preg_replace('/^' . preg_quote($request->base, '/') . '/', '', $url, 1); - } - $url = '/' . $url; - - while (strpos($url, '//') !== false) { - $url = str_replace('//', '/', $url); - } - $url = preg_replace('/(?:(\/$))/', '', $url); - - if (empty($url)) { - return '/'; - } - return $url; - } - -/** - * Returns the route matching the current request URL. - * - * @return CakeRoute Matching route object. - */ - public static function requestRoute() { - return static::$_currentRoute[0]; - } - -/** - * Returns the route matching the current request (useful for requestAction traces) - * - * @return CakeRoute Matching route object. - */ - public static function currentRoute() { - $count = count(static::$_currentRoute) - 1; - return ($count >= 0) ? static::$_currentRoute[$count] : false; - } - -/** - * Removes the plugin name from the base URL. - * - * @param string $base Base URL - * @param string $plugin Plugin name - * @return string base URL with plugin name removed if present - */ - public static function stripPlugin($base, $plugin = null) { - if ($plugin) { - $base = preg_replace('/(?:' . $plugin . ')/', '', $base); - $base = str_replace('//', '', $base); - $pos1 = strrpos($base, '/'); - $char = strlen($base) - 1; - - if ($pos1 === $char) { - $base = substr($base, 0, $char); - } - } - return $base; - } - -/** - * Instructs the router to parse out file extensions from the URL. - * - * For example, http://example.com/posts.rss would yield a file extension of "rss". - * The file extension itself is made available in the controller as - * `$this->params['ext']`, and is used by the RequestHandler component to - * automatically switch to alternate layouts and templates, and load helpers - * corresponding to the given content, i.e. RssHelper. Switching layouts and helpers - * requires that the chosen extension has a defined mime type in `CakeResponse` - * - * A list of valid extension can be passed to this method, i.e. Router::parseExtensions('rss', 'xml'); - * If no parameters are given, anything after the first . (dot) after the last / in the URL will be - * parsed, excluding querystring parameters (i.e. ?q=...). - * - * @return void - * @see RequestHandler::startup() - */ - public static function parseExtensions() { - static::$_parseExtensions = true; - if (func_num_args() > 0) { - static::setExtensions(func_get_args(), false); - } - } - -/** - * Get the list of extensions that can be parsed by Router. - * - * To initially set extensions use `Router::parseExtensions()` - * To add more see `setExtensions()` - * - * @return array Array of extensions Router is configured to parse. - */ - public static function extensions() { - if (!static::$initialized) { - static::_loadRoutes(); - } - - return static::$_validExtensions; - } - -/** - * Set/add valid extensions. - * - * To have the extensions parsed you still need to call `Router::parseExtensions()` - * - * @param array $extensions List of extensions to be added as valid extension - * @param bool $merge Default true will merge extensions. Set to false to override current extensions - * @return array - */ - public static function setExtensions($extensions, $merge = true) { - if (!is_array($extensions)) { - return static::$_validExtensions; - } - if (!$merge) { - return static::$_validExtensions = $extensions; - } - return static::$_validExtensions = array_merge(static::$_validExtensions, $extensions); - } - -/** - * Loads route configuration - * - * @return void - */ - protected static function _loadRoutes() { - static::$initialized = true; - include CONFIG . 'routes.php'; - } +class Router +{ + + /** + * Regular expression for action names + * + * @var string + */ + const ACTION = 'index|show|add|create|edit|update|remove|del|delete|view|item'; + /** + * Regular expression for years + * + * @var string + */ + const YEAR = '[12][0-9]{3}'; + /** + * Regular expression for months + * + * @var string + */ + const MONTH = '0[1-9]|1[012]'; + /** + * Regular expression for days + * + * @var string + */ + const DAY = '0[1-9]|[12][0-9]|3[01]'; + /** + * Regular expression for auto increment IDs + * + * @var string + */ + const ID = '[0-9]+'; + /** + * Regular expression for UUIDs + * + * @var string + */ + const UUID = '[A-Fa-f0-9]{8}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{12}'; + /** + * Array of routes connected with Router::connect() + * + * @var array + */ + public static $routes = []; + /** + * Have routes been loaded + * + * @var bool + */ + public static $initialized = false; + /** + * Contains the base string that will be applied to all generated URLs + * For example `https://example.com` + * + * @var string + */ + protected static $_fullBaseUrl; + /** + * List of action prefixes used in connected routes. + * Includes admin prefix + * + * @var array + */ + protected static $_prefixes = []; + /** + * Directive for Router to parse out file extensions for mapping to Content-types. + * + * @var bool + */ + protected static $_parseExtensions = false; + /** + * List of valid extensions to parse from a URL. If null, any extension is allowed. + * + * @var array + */ + protected static $_validExtensions = []; + /** + * Named expressions + * + * @var array + */ + protected static $_namedExpressions = [ + 'Action' => Router::ACTION, + 'Year' => Router::YEAR, + 'Month' => Router::MONTH, + 'Day' => Router::DAY, + 'ID' => Router::ID, + 'UUID' => Router::UUID + ]; + + /** + * Stores all information necessary to decide what named arguments are parsed under what conditions. + * + * @var string + */ + protected static $_namedConfig = [ + 'default' => ['page', 'fields', 'order', 'limit', 'recursive', 'sort', 'direction', 'step'], + 'greedyNamed' => true, + 'separator' => ':', + 'rules' => false, + ]; + + /** + * The route matching the URL of the current request + * + * @var array + */ + protected static $_currentRoute = []; + + /** + * Default HTTP request method => controller action map. + * + * @var array + */ + protected static $_resourceMap = [ + ['action' => 'index', 'method' => 'GET', 'id' => false], + ['action' => 'view', 'method' => 'GET', 'id' => true], + ['action' => 'add', 'method' => 'POST', 'id' => false], + ['action' => 'edit', 'method' => 'PUT', 'id' => true], + ['action' => 'delete', 'method' => 'DELETE', 'id' => true], + ['action' => 'edit', 'method' => 'POST', 'id' => true] + ]; + + /** + * List of resource-mapped controllers + * + * @var array + */ + protected static $_resourceMapped = []; + + /** + * Maintains the request object stack for the current request. + * This will contain more than one request object when requestAction is used. + * + * @var array + */ + protected static $_requests = []; + + /** + * Initial state is populated the first time reload() is called which is at the bottom + * of this file. This is a cheat as get_class_vars() returns the value of static vars even if they + * have changed. + * + * @var array + */ + protected static $_initialState = []; + + /** + * Default route class to use + * + * @var string + */ + protected static $_routeClass = 'CakeRoute'; + + /** + * Set the default route class to use or return the current one + * + * @param string $routeClass The route class to set as default. + * @return string|null The default route class. + * @throws RouterException + */ + public static function defaultRouteClass($routeClass = null) + { + if ($routeClass === null) { + return static::$_routeClass; + } + + static::$_routeClass = static::_validateRouteClass($routeClass); + } + + /** + * Validates that the passed route class exists and is a subclass of CakeRoute + * + * @param string $routeClass Route class name + * @return string + * @throws RouterException + */ + protected static function _validateRouteClass($routeClass) + { + if ($routeClass !== 'CakeRoute' && + (!class_exists($routeClass) || !is_subclass_of($routeClass, 'CakeRoute')) + ) { + throw new RouterException(__d('cake_dev', 'Route class not found, or route class is not a subclass of CakeRoute')); + } + return $routeClass; + } + + /** + * Gets the named route elements for use in app/Config/routes.php + * + * @return array Named route elements + * @see Router::$_namedExpressions + */ + public static function getNamedExpressions() + { + return static::$_namedExpressions; + } + + /** + * Resource map getter & setter. + * + * @param array $resourceMap Resource map + * @return mixed + * @see Router::$_resourceMap + */ + public static function resourceMap($resourceMap = null) + { + if ($resourceMap === null) { + return static::$_resourceMap; + } + static::$_resourceMap = $resourceMap; + } + + /** + * Connects a new redirection Route in the router. + * + * Redirection routes are different from normal routes as they perform an actual + * header redirection if a match is found. The redirection can occur within your + * application or redirect to an outside location. + * + * Examples: + * + * `Router::redirect('/home/*', array('controller' => 'posts', 'action' => 'view'), array('persist' => true));` + * + * Redirects /home/* to /posts/view and passes the parameters to /posts/view. Using an array as the + * redirect destination allows you to use other routes to define where a URL string should be redirected to. + * + * `Router::redirect('/posts/*', 'http://google.com', array('status' => 302));` + * + * Redirects /posts/* to http://google.com with a HTTP status of 302 + * + * ### Options: + * + * - `status` Sets the HTTP status (default 301) + * - `persist` Passes the params to the redirected route, if it can. This is useful with greedy routes, + * routes that end in `*` are greedy. As you can remap URLs and not loose any passed/named args. + * + * @param string $route A string describing the template of the route + * @param array $url A URL to redirect to. Can be a string or a CakePHP array-based URL + * @param array $options An array matching the named elements in the route to regular expressions which that + * element should match. Also contains additional parameters such as which routed parameters should be + * shifted into the passed arguments. As well as supplying patterns for routing parameters. + * @return array Array of routes + * @see routes + */ + public static function redirect($route, $url, $options = []) + { + App::uses('RedirectRoute', 'Routing/Route'); + $options['routeClass'] = 'RedirectRoute'; + if (is_string($url)) { + $url = ['redirect' => $url]; + } + return static::connect($route, $url, $options); + } + + /** + * Connects a new Route in the router. + * + * Routes are a way of connecting request URLs to objects in your application. At their core routes + * are a set of regular expressions that are used to match requests to destinations. + * + * Examples: + * + * `Router::connect('/:controller/:action/*');` + * + * The first token ':controller' will be used as a controller name while the second is used as the action name. + * the '/*' syntax makes this route greedy in that it will match requests like `/posts/index` as well as requests + * like `/posts/edit/1/foo/bar`. + * + * `Router::connect('/home-page', array('controller' => 'pages', 'action' => 'display', 'home'));` + * + * The above shows the use of route parameter defaults, and providing routing parameters for a static route. + * + * ``` + * Router::connect( + * '/:lang/:controller/:action/:id', + * array(), + * array('id' => '[0-9]+', 'lang' => '[a-z]{3}') + * ); + * ``` + * + * Shows connecting a route with custom route parameters as well as providing patterns for those parameters. + * Patterns for routing parameters do not need capturing groups, as one will be added for each route params. + * + * $defaults is merged with the results of parsing the request URL to form the final routing destination and its + * parameters. This destination is expressed as an associative array by Router. See the output of {@link parse()}. + * + * $options offers four 'special' keys. `pass`, `named`, `persist` and `routeClass` + * have special meaning in the $options array. + * + * - `pass` is used to define which of the routed parameters should be shifted into the pass array. Adding a + * parameter to pass will remove it from the regular route array. Ex. `'pass' => array('slug')` + * - `persist` is used to define which route parameters should be automatically included when generating + * new URLs. You can override persistent parameters by redefining them in a URL or remove them by + * setting the parameter to `false`. Ex. `'persist' => array('lang')` + * - `routeClass` is used to extend and change how individual routes parse requests and handle reverse routing, + * via a custom routing class. Ex. `'routeClass' => 'SlugRoute'` + * - `named` is used to configure named parameters at the route level. This key uses the same options + * as Router::connectNamed() + * + * You can also add additional conditions for matching routes to the $defaults array. + * The following conditions can be used: + * + * - `[type]` Only match requests for specific content types. + * - `[method]` Only match requests with specific HTTP verbs. + * - `[server]` Only match when $_SERVER['SERVER_NAME'] matches the given value. + * + * Example of using the `[method]` condition: + * + * `Router::connect('/tasks', array('controller' => 'tasks', 'action' => 'index', '[method]' => 'GET'));` + * + * The above route will only be matched for GET requests. POST requests will fail to match this route. + * + * @param string $route A string describing the template of the route + * @param array $defaults An array describing the default route parameters. These parameters will be used by default + * and can supply routing parameters that are not dynamic. See above. + * @param array $options An array matching the named elements in the route to regular expressions which that + * element should match. Also contains additional parameters such as which routed parameters should be + * shifted into the passed arguments, supplying patterns for routing parameters and supplying the name of a + * custom routing class. + * @return array Array of routes + * @throws RouterException + * @see routes + * @see parse(). + */ + public static function connect($route, $defaults = [], $options = []) + { + static::$initialized = true; + + foreach (static::$_prefixes as $prefix) { + if (isset($defaults[$prefix])) { + if ($defaults[$prefix]) { + $defaults['prefix'] = $prefix; + } else { + unset($defaults[$prefix]); + } + break; + } + } + if (isset($defaults['prefix']) && !in_array($defaults['prefix'], static::$_prefixes)) { + static::$_prefixes[] = $defaults['prefix']; + } + $defaults += ['plugin' => null]; + if (empty($options['action'])) { + $defaults += ['action' => 'index']; + } + $routeClass = static::$_routeClass; + if (isset($options['routeClass'])) { + if (strpos($options['routeClass'], '.') === false) { + $routeClass = $options['routeClass']; + } else { + list(, $routeClass) = pluginSplit($options['routeClass'], true); + } + $routeClass = static::_validateRouteClass($routeClass); + unset($options['routeClass']); + } + if ($routeClass === 'RedirectRoute' && isset($defaults['redirect'])) { + $defaults = $defaults['redirect']; + } + static::$routes[] = new $routeClass($route, $defaults, $options); + return static::$routes; + } + + /** + * Specifies what named parameters CakePHP should be parsing out of incoming URLs. By default + * CakePHP will parse every named parameter out of incoming URLs. However, if you want to take more + * control over how named parameters are parsed you can use one of the following setups: + * + * Do not parse any named parameters: + * + * ``` Router::connectNamed(false); ``` + * + * Parse only default parameters used for CakePHP's pagination: + * + * ``` Router::connectNamed(false, array('default' => true)); ``` + * + * Parse only the page parameter if its value is a number: + * + * ``` Router::connectNamed(array('page' => '[\d]+'), array('default' => false, 'greedy' => false)); ``` + * + * Parse only the page parameter no matter what. + * + * ``` Router::connectNamed(array('page'), array('default' => false, 'greedy' => false)); ``` + * + * Parse only the page parameter if the current action is 'index'. + * + * ``` + * Router::connectNamed( + * array('page' => array('action' => 'index')), + * array('default' => false, 'greedy' => false) + * ); + * ``` + * + * Parse only the page parameter if the current action is 'index' and the controller is 'pages'. + * + * ``` + * Router::connectNamed( + * array('page' => array('action' => 'index', 'controller' => 'pages')), + * array('default' => false, 'greedy' => false) + * ); + * ``` + * + * ### Options + * + * - `greedy` Setting this to true will make Router parse all named params. Setting it to false will + * parse only the connected named params. + * - `default` Set this to true to merge in the default set of named parameters. + * - `reset` Set to true to clear existing rules and start fresh. + * - `separator` Change the string used to separate the key & value in a named parameter. Defaults to `:` + * + * @param array $named A list of named parameters. Key value pairs are accepted where values are + * either regex strings to match, or arrays as seen above. + * @param array $options Allows to control all settings: separator, greedy, reset, default + * @return array + */ + public static function connectNamed($named, $options = []) + { + if (isset($options['separator'])) { + static::$_namedConfig['separator'] = $options['separator']; + unset($options['separator']); + } + + if ($named === true || $named === false) { + $options += ['default' => $named, 'reset' => true, 'greedy' => $named]; + $named = []; + } else { + $options += ['default' => false, 'reset' => false, 'greedy' => true]; + } + + if ($options['reset'] || static::$_namedConfig['rules'] === false) { + static::$_namedConfig['rules'] = []; + } + + if ($options['default']) { + $named = array_merge($named, static::$_namedConfig['default']); + } + + foreach ($named as $key => $val) { + if (is_numeric($key)) { + static::$_namedConfig['rules'][$val] = true; + } else { + static::$_namedConfig['rules'][$key] = $val; + } + } + static::$_namedConfig['greedyNamed'] = $options['greedy']; + return static::$_namedConfig; + } + + /** + * Gets the current named parameter configuration values. + * + * @return array + * @see Router::$_namedConfig + */ + public static function namedConfig() + { + return static::$_namedConfig; + } + + /** + * Creates REST resource routes for the given controller(s). When creating resource routes + * for a plugin, by default the prefix will be changed to the lower_underscore version of the plugin + * name. By providing a prefix you can override this behavior. + * + * ### Options: + * + * - 'id' - The regular expression fragment to use when matching IDs. By default, matches + * integer values and UUIDs. + * - 'prefix' - URL prefix to use for the generated routes. Defaults to '/'. + * - 'connectOptions' – Custom options for connecting the routes. + * + * @param string|array $controller A controller name or array of controller names (i.e. "Posts" or "ListItems") + * @param array $options Options to use when generating REST routes + * @return array Array of mapped resources + */ + public static function mapResources($controller, $options = []) + { + $hasPrefix = isset($options['prefix']); + $options += [ + 'connectOptions' => [], + 'prefix' => '/', + 'id' => static::ID . '|' . static::UUID + ]; + + $prefix = $options['prefix']; + $connectOptions = $options['connectOptions']; + unset($options['connectOptions']); + if (strpos($prefix, '/') !== 0) { + $prefix = '/' . $prefix; + } + if (substr($prefix, -1) !== '/') { + $prefix .= '/'; + } + + foreach ((array)$controller as $name) { + list($plugin, $name) = pluginSplit($name); + $urlName = Inflector::underscore($name); + $plugin = Inflector::underscore($plugin); + if ($plugin && !$hasPrefix) { + $prefix = '/' . $plugin . '/'; + } + + foreach (static::$_resourceMap as $params) { + $url = $prefix . $urlName . (($params['id']) ? '/:id' : ''); + + Router::connect($url, + [ + 'plugin' => $plugin, + 'controller' => $urlName, + 'action' => $params['action'], + '[method]' => $params['method'] + ], + array_merge( + ['id' => $options['id'], 'pass' => ['id']], + $connectOptions + ) + ); + } + static::$_resourceMapped[] = $urlName; + } + return static::$_resourceMapped; + } + + /** + * Returns the list of prefixes used in connected routes + * + * @return array A list of prefixes used in connected routes + */ + public static function prefixes() + { + return static::$_prefixes; + } + + /** + * Parses given URL string. Returns 'routing' parameters for that URL. + * + * @param string $url URL to be parsed + * @return array Parsed elements from URL + */ + public static function parse($url) + { + if (!static::$initialized) { + static::_loadRoutes(); + } + + $ext = null; + $out = []; + + if (strlen($url) && strpos($url, '/') !== 0) { + $url = '/' . $url; + } + if (strpos($url, '?') !== false) { + list($url, $queryParameters) = explode('?', $url, 2); + parse_str($queryParameters, $queryParameters); + } + + extract(static::_parseExtension($url)); + + foreach (static::$routes as $route) { + if (($r = $route->parse($url)) !== false) { + static::$_currentRoute[] = $route; + $out = $r; + break; + } + } + if (isset($out['prefix'])) { + $out['action'] = $out['prefix'] . '_' . $out['action']; + } + + if (!empty($ext) && !isset($out['ext'])) { + $out['ext'] = $ext; + } + + if (!empty($queryParameters) && !isset($out['?'])) { + $out['?'] = $queryParameters; + } + return $out; + } + + /** + * Loads route configuration + * + * @return void + */ + protected static function _loadRoutes() + { + static::$initialized = true; + include CONFIG . 'routes.php'; + } + + /** + * Parses a file extension out of a URL, if Router::parseExtensions() is enabled. + * + * @param string $url URL. + * @return array Returns an array containing the altered URL and the parsed extension. + */ + protected static function _parseExtension($url) + { + $ext = null; + + if (static::$_parseExtensions) { + if (preg_match('/\.[0-9a-zA-Z]*$/', $url, $match) === 1) { + $match = substr($match[0], 1); + if (empty(static::$_validExtensions)) { + $url = substr($url, 0, strpos($url, '.' . $match)); + $ext = $match; + } else { + foreach (static::$_validExtensions as $name) { + if (strcasecmp($name, $match) === 0) { + $url = substr($url, 0, strpos($url, '.' . $name)); + $ext = $match; + break; + } + } + } + } + } + return compact('ext', 'url'); + } + + /** + * Takes parameter and path information back from the Dispatcher, sets these + * parameters as the current request parameters that are merged with URL arrays + * created later in the request. + * + * Nested requests will create a stack of requests. You can remove requests using + * Router::popRequest(). This is done automatically when using CakeObject::requestAction(). + * + * Will accept either a CakeRequest object or an array of arrays. Support for + * accepting arrays may be removed in the future. + * + * @param CakeRequest|array $request Parameters and path information or a CakeRequest object. + * @return void + */ + public static function setRequestInfo($request) + { + if ($request instanceof CakeRequest) { + static::$_requests[] = $request; + } else { + $requestObj = new CakeRequest(); + $request += [[], []]; + $request[0] += ['controller' => false, 'action' => false, 'plugin' => null]; + $requestObj->addParams($request[0])->addPaths($request[1]); + static::$_requests[] = $requestObj; + } + } + + /** + * Pops a request off of the request stack. Used when doing requestAction + * + * @return CakeRequest The request removed from the stack. + * @see Router::setRequestInfo() + * @see Object::requestAction() + */ + public static function popRequest() + { + return array_pop(static::$_requests); + } + + /** + * Gets URL parameter by name + * + * @param string $name Parameter name + * @param bool $current Current parameter, useful when using requestAction + * @return string|null Parameter value + */ + public static function getParam($name = 'controller', $current = false) + { + $params = Router::getParams($current); + if (isset($params[$name])) { + return $params[$name]; + } + return null; + } + + /** + * Gets parameter information + * + * @param bool $current Get current request parameter, useful when using requestAction + * @return array Parameter information + */ + public static function getParams($current = false) + { + if ($current && static::$_requests) { + return static::$_requests[count(static::$_requests) - 1]->params; + } + if (isset(static::$_requests[0])) { + return static::$_requests[0]->params; + } + return []; + } + + /** + * Gets path information + * + * @param bool $current Current parameter, useful when using requestAction + * @return array + */ + public static function getPaths($current = false) + { + if ($current) { + return static::$_requests[count(static::$_requests) - 1]; + } + if (!isset(static::$_requests[0])) { + return ['base' => null]; + } + return ['base' => static::$_requests[0]->base]; + } + + /** + * Reloads default Router settings. Resets all class variables and + * removes all connected routes. + * + * @return void + */ + public static function reload() + { + if (empty(static::$_initialState)) { + static::$_initialState = get_class_vars('Router'); + static::_setPrefixes(); + return; + } + foreach (static::$_initialState as $key => $val) { + if ($key !== '_initialState') { + static::${$key} = $val; + } + } + static::_setPrefixes(); + } + + /** + * Sets the Routing prefixes. + * + * @return void + */ + protected static function _setPrefixes() + { + $routing = Configure::read('Routing'); + if (!empty($routing['prefixes'])) { + static::$_prefixes = array_merge(static::$_prefixes, (array)$routing['prefixes']); + } + } + + /** + * Promote a route (by default, the last one added) to the beginning of the list + * + * @param int $which A zero-based array index representing the route to move. For example, + * if 3 routes have been added, the last route would be 2. + * @return bool Returns false if no route exists at the position specified by $which. + */ + public static function promote($which = null) + { + if ($which === null) { + $which = count(static::$routes) - 1; + } + if (!isset(static::$routes[$which])) { + return false; + } + $route =& static::$routes[$which]; + unset(static::$routes[$which]); + array_unshift(static::$routes, $route); + return true; + } + + /** + * Reverses a parsed parameter array into a string. + * + * Works similarly to Router::url(), but since parsed URL's contain additional + * 'pass' and 'named' as well as 'url.url' keys. Those keys need to be specially + * handled in order to reverse a params array into a string URL. + * + * This will strip out 'autoRender', 'bare', 'requested', and 'return' param names as those + * are used for CakePHP internals and should not normally be part of an output URL. + * + * @param CakeRequest|array $params The params array or CakeRequest object that needs to be reversed. + * @param bool $full Set to true to include the full URL including the protocol when reversing + * the URL. + * @return string The string that is the reversed result of the array + */ + public static function reverse($params, $full = false) + { + $params = Router::reverseToArray($params, $full); + return Router::url($params, $full); + } + + /** + * Reverses a parsed parameter array into an array. + * + * Works similarly to Router::url(), but since parsed URL's contain additional + * 'pass' and 'named' as well as 'url.url' keys. Those keys need to be specially + * handled in order to reverse a params array into a string URL. + * + * This will strip out 'autoRender', 'bare', 'requested', and 'return' param names as those + * are used for CakePHP internals and should not normally be part of an output URL. + * + * @param CakeRequest|array $params The params array or CakeRequest object that needs to be reversed. + * @return array The URL array ready to be used for redirect or HTML link. + */ + public static function reverseToArray($params) + { + if ($params instanceof CakeRequest) { + $url = $params->query; + $params = $params->params; + } else { + $url = $params['url']; + } + $pass = isset($params['pass']) ? $params['pass'] : []; + $named = isset($params['named']) ? $params['named'] : []; + unset( + $params['pass'], $params['named'], $params['paging'], $params['models'], $params['url'], $url['url'], + $params['autoRender'], $params['bare'], $params['requested'], $params['return'], + $params['_Token'] + ); + $params = array_merge($params, $pass, $named); + if (!empty($url)) { + $params['?'] = $url; + } + return $params; + } + + /** + * Finds URL for specified action. + * + * Returns a URL pointing to a combination of controller and action. Param + * $url can be: + * + * - Empty - the method will find address to actual controller/action. + * - '/' - the method will find base URL of application. + * - A combination of controller/action - the method will find URL for it. + * + * There are a few 'special' parameters that can change the final URL string that is generated + * + * - `base` - Set to false to remove the base path from the generated URL. If your application + * is not in the root directory, this can be used to generate URLs that are 'cake relative'. + * cake relative URLs are required when using requestAction. + * - `?` - Takes an array of query string parameters + * - `#` - Allows you to set URL hash fragments. + * - `full_base` - If true the `Router::fullBaseUrl()` value will be prepended to generated URLs. + * + * @param string|array $url Cake-relative URL, like "/products/edit/92" or "/presidents/elect/4" + * or an array specifying any of the following: 'controller', 'action', + * and/or 'plugin', in addition to named arguments (keyed array elements), + * and standard URL arguments (indexed array elements) + * @param bool|array $full If (bool) true, the full base URL will be prepended to the result. + * If an array accepts the following keys + * - escape - used when making URLs embedded in html escapes query string '&' + * - full - if true the full base URL will be prepended. + * @return string Full translated URL with base path. + */ + public static function url($url = null, $full = false) + { + if (!static::$initialized) { + static::_loadRoutes(); + } + + $params = ['plugin' => null, 'controller' => null, 'action' => 'index']; + + if (is_bool($full)) { + $escape = false; + } else { + extract($full + ['escape' => false, 'full' => false]); + } + + $path = ['base' => null]; + if (!empty(static::$_requests)) { + $request = static::$_requests[count(static::$_requests) - 1]; + $params = $request->params; + $path = ['base' => $request->base, 'here' => $request->here]; + } + if (empty($path['base'])) { + $path['base'] = Configure::read('App.base'); + } + + $base = $path['base']; + $extension = $output = $q = $frag = null; + + if (empty($url)) { + $output = isset($path['here']) ? $path['here'] : '/'; + if ($full) { + $output = static::fullBaseUrl() . $output; + } + return $output; + } else if (is_array($url)) { + if (isset($url['base']) && $url['base'] === false) { + $base = null; + unset($url['base']); + } + if (isset($url['full_base']) && $url['full_base'] === true) { + $full = true; + unset($url['full_base']); + } + if (isset($url['?'])) { + $q = $url['?']; + unset($url['?']); + } + if (isset($url['#'])) { + $frag = '#' . $url['#']; + unset($url['#']); + } + if (isset($url['ext'])) { + $extension = '.' . $url['ext']; + unset($url['ext']); + } + if (empty($url['action'])) { + if (empty($url['controller']) || $params['controller'] === $url['controller']) { + $url['action'] = $params['action']; + } else { + $url['action'] = 'index'; + } + } + + $prefixExists = (array_intersect_key($url, array_flip(static::$_prefixes))); + foreach (static::$_prefixes as $prefix) { + if (!empty($params[$prefix]) && !$prefixExists) { + $url[$prefix] = true; + } else if (isset($url[$prefix]) && !$url[$prefix]) { + unset($url[$prefix]); + } + if (isset($url[$prefix]) && strpos($url['action'], $prefix . '_') === 0) { + $url['action'] = substr($url['action'], strlen($prefix) + 1); + } + } + + $url += ['controller' => $params['controller'], 'plugin' => $params['plugin']]; + + $match = false; + + foreach (static::$routes as $route) { + $originalUrl = $url; + + $url = $route->persistParams($url, $params); + + if ($match = $route->match($url)) { + $output = trim($match, '/'); + break; + } + $url = $originalUrl; + } + if ($match === false) { + $output = static::_handleNoRoute($url); + } + } else { + if (preg_match('/^([a-z][a-z0-9.+\-]+:|:?\/\/|[#?])/i', $url)) { + return $url; + } + if (substr($url, 0, 1) === '/') { + $output = substr($url, 1); + } else { + foreach (static::$_prefixes as $prefix) { + if (isset($params[$prefix])) { + $output .= $prefix . '/'; + break; + } + } + if (!empty($params['plugin']) && $params['plugin'] !== $params['controller']) { + $output .= Inflector::underscore($params['plugin']) . '/'; + } + $output .= Inflector::underscore($params['controller']) . '/' . $url; + } + } + $protocol = preg_match('#^[a-z][a-z0-9+\-.]*\://#i', $output); + if ($protocol === 0) { + $output = str_replace('//', '/', $base . '/' . $output); + + if ($full) { + $output = static::fullBaseUrl() . $output; + } + if (!empty($extension)) { + $output = rtrim($output, '/'); + } + } + return $output . $extension . static::queryString($q, [], $escape) . $frag; + } + + /** + * Sets the full base URL that will be used as a prefix for generating + * fully qualified URLs for this application. If no parameters are passed, + * the currently configured value is returned. + * + * ## Note: + * + * If you change the configuration value ``App.fullBaseUrl`` during runtime + * and expect the router to produce links using the new setting, you are + * required to call this method passing such value again. + * + * @param string $base the prefix for URLs generated containing the domain. + * For example: ``http://example.com`` + * @return string + */ + public static function fullBaseUrl($base = null) + { + if ($base !== null) { + static::$_fullBaseUrl = $base; + Configure::write('App.fullBaseUrl', $base); + } + if (empty(static::$_fullBaseUrl)) { + static::$_fullBaseUrl = Configure::read('App.fullBaseUrl'); + } + return static::$_fullBaseUrl; + } + + /** + * A special fallback method that handles URL arrays that cannot match + * any defined routes. + * + * @param array $url A URL that didn't match any routes + * @return string A generated URL for the array + * @see Router::url() + */ + protected static function _handleNoRoute($url) + { + $named = $args = []; + $skip = array_merge( + ['bare', 'action', 'controller', 'plugin', 'prefix'], + static::$_prefixes + ); + + $keys = array_values(array_diff(array_keys($url), $skip)); + + // Remove this once parsed URL parameters can be inserted into 'pass' + foreach ($keys as $key) { + if (is_numeric($key)) { + $args[] = $url[$key]; + } else { + $named[$key] = $url[$key]; + } + } + + list($args, $named) = [Hash::filter($args), Hash::filter($named)]; + foreach (static::$_prefixes as $prefix) { + $prefixed = $prefix . '_'; + if (!empty($url[$prefix]) && strpos($url['action'], $prefixed) === 0) { + $url['action'] = substr($url['action'], strlen($prefixed) * -1); + break; + } + } + + if (empty($named) && empty($args) && (!isset($url['action']) || $url['action'] === 'index')) { + $url['action'] = null; + } + + $urlOut = array_filter([$url['controller'], $url['action']]); + + if (isset($url['plugin'])) { + array_unshift($urlOut, $url['plugin']); + } + + foreach (static::$_prefixes as $prefix) { + if (isset($url[$prefix])) { + array_unshift($urlOut, $prefix); + break; + } + } + $output = implode('/', $urlOut); + + if (!empty($args)) { + $output .= '/' . implode('/', array_map('rawurlencode', $args)); + } + + if (!empty($named)) { + foreach ($named as $name => $value) { + if (is_array($value)) { + $flattend = Hash::flatten($value, '%5D%5B'); + foreach ($flattend as $namedKey => $namedValue) { + $output .= '/' . $name . "%5B{$namedKey}%5D" . static::$_namedConfig['separator'] . rawurlencode($namedValue); + } + } else { + $output .= '/' . $name . static::$_namedConfig['separator'] . rawurlencode($value); + } + } + } + return $output; + } + + /** + * Generates a well-formed querystring from $q + * + * @param string|array $q Query string Either a string of already compiled query string arguments or + * an array of arguments to convert into a query string. + * @param array $extra Extra querystring parameters. + * @param bool $escape Whether or not to use escaped & + * @return string|null + */ + public static function queryString($q, $extra = [], $escape = false) + { + if (empty($q) && empty($extra)) { + return null; + } + $join = '&'; + if ($escape === true) { + $join = '&'; + } + $out = ''; + + if (is_array($q)) { + $q = array_merge($q, $extra); + } else { + $out = $q; + $q = $extra; + } + $addition = http_build_query($q, null, $join); + + if ($out && $addition && substr($out, strlen($join) * -1, strlen($join)) !== $join) { + $out .= $join; + } + + $out .= $addition; + + if (isset($out[0]) && $out[0] !== '?') { + $out = '?' . $out; + } + return $out; + } + + /** + * Normalizes a URL for purposes of comparison. + * + * Will strip the base path off and replace any double /'s. + * It will not unify the casing and underscoring of the input value. + * + * @param array|string $url URL to normalize Either an array or a string URL. + * @return string Normalized URL + */ + public static function normalize($url = '/') + { + if (is_array($url)) { + $url = Router::url($url); + } + if (preg_match('/^[a-z\-]+:\/\//', $url)) { + return $url; + } + $request = Router::getRequest(); + + if (!empty($request->base) && stristr($url, $request->base)) { + $url = preg_replace('/^' . preg_quote($request->base, '/') . '/', '', $url, 1); + } + $url = '/' . $url; + + while (strpos($url, '//') !== false) { + $url = str_replace('//', '/', $url); + } + $url = preg_replace('/(?:(\/$))/', '', $url); + + if (empty($url)) { + return '/'; + } + return $url; + } + + /** + * Gets the current request object, or the first one. + * + * @param bool $current True to get the current request object, or false to get the first one. + * @return CakeRequest|null Null if stack is empty. + */ + public static function getRequest($current = false) + { + if ($current) { + $i = count(static::$_requests) - 1; + return isset(static::$_requests[$i]) ? static::$_requests[$i] : null; + } + return isset(static::$_requests[0]) ? static::$_requests[0] : null; + } + + /** + * Returns the route matching the current request URL. + * + * @return CakeRoute Matching route object. + */ + public static function requestRoute() + { + return static::$_currentRoute[0]; + } + + /** + * Returns the route matching the current request (useful for requestAction traces) + * + * @return CakeRoute Matching route object. + */ + public static function currentRoute() + { + $count = count(static::$_currentRoute) - 1; + return ($count >= 0) ? static::$_currentRoute[$count] : false; + } + + /** + * Removes the plugin name from the base URL. + * + * @param string $base Base URL + * @param string $plugin Plugin name + * @return string base URL with plugin name removed if present + */ + public static function stripPlugin($base, $plugin = null) + { + if ($plugin) { + $base = preg_replace('/(?:' . $plugin . ')/', '', $base); + $base = str_replace('//', '', $base); + $pos1 = strrpos($base, '/'); + $char = strlen($base) - 1; + + if ($pos1 === $char) { + $base = substr($base, 0, $char); + } + } + return $base; + } + + /** + * Instructs the router to parse out file extensions from the URL. + * + * For example, http://example.com/posts.rss would yield a file extension of "rss". + * The file extension itself is made available in the controller as + * `$this->params['ext']`, and is used by the RequestHandler component to + * automatically switch to alternate layouts and templates, and load helpers + * corresponding to the given content, i.e. RssHelper. Switching layouts and helpers + * requires that the chosen extension has a defined mime type in `CakeResponse` + * + * A list of valid extension can be passed to this method, i.e. Router::parseExtensions('rss', 'xml'); + * If no parameters are given, anything after the first . (dot) after the last / in the URL will be + * parsed, excluding querystring parameters (i.e. ?q=...). + * + * @return void + * @see RequestHandler::startup() + */ + public static function parseExtensions() + { + static::$_parseExtensions = true; + if (func_num_args() > 0) { + static::setExtensions(func_get_args(), false); + } + } + + /** + * Set/add valid extensions. + * + * To have the extensions parsed you still need to call `Router::parseExtensions()` + * + * @param array $extensions List of extensions to be added as valid extension + * @param bool $merge Default true will merge extensions. Set to false to override current extensions + * @return array + */ + public static function setExtensions($extensions, $merge = true) + { + if (!is_array($extensions)) { + return static::$_validExtensions; + } + if (!$merge) { + return static::$_validExtensions = $extensions; + } + return static::$_validExtensions = array_merge(static::$_validExtensions, $extensions); + } + + /** + * Get the list of extensions that can be parsed by Router. + * + * To initially set extensions use `Router::parseExtensions()` + * To add more see `setExtensions()` + * + * @return array Array of extensions Router is configured to parse. + */ + public static function extensions() + { + if (!static::$initialized) { + static::_loadRoutes(); + } + + return static::$_validExtensions; + } } diff --git a/lib/Cake/Utility/CakeNumber.php b/lib/Cake/Utility/CakeNumber.php index 7aa05d52..f8ae1ce1 100755 --- a/lib/Cake/Utility/CakeNumber.php +++ b/lib/Cake/Utility/CakeNumber.php @@ -26,391 +26,402 @@ * @package Cake.Utility * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/number.html */ -class CakeNumber { - -/** - * Currencies supported by the helper. You can add additional currency formats - * with CakeNumber::addFormat - * - * @var array - */ - protected static $_currencies = array( - 'AUD' => array( - 'wholeSymbol' => '$', 'wholePosition' => 'before', 'fractionSymbol' => 'c', 'fractionPosition' => 'after', - 'zero' => 0, 'places' => 2, 'thousands' => ',', 'decimals' => '.', 'negative' => '()', 'escape' => true, - 'fractionExponent' => 2 - ), - 'CAD' => array( - 'wholeSymbol' => '$', 'wholePosition' => 'before', 'fractionSymbol' => 'c', 'fractionPosition' => 'after', - 'zero' => 0, 'places' => 2, 'thousands' => ',', 'decimals' => '.', 'negative' => '()', 'escape' => true, - 'fractionExponent' => 2 - ), - 'USD' => array( - 'wholeSymbol' => '$', 'wholePosition' => 'before', 'fractionSymbol' => 'c', 'fractionPosition' => 'after', - 'zero' => 0, 'places' => 2, 'thousands' => ',', 'decimals' => '.', 'negative' => '()', 'escape' => true, - 'fractionExponent' => 2 - ), - 'EUR' => array( - 'wholeSymbol' => '€', 'wholePosition' => 'before', 'fractionSymbol' => false, 'fractionPosition' => 'after', - 'zero' => 0, 'places' => 2, 'thousands' => '.', 'decimals' => ',', 'negative' => '()', 'escape' => true, - 'fractionExponent' => 0 - ), - 'GBP' => array( - 'wholeSymbol' => '£', 'wholePosition' => 'before', 'fractionSymbol' => 'p', 'fractionPosition' => 'after', - 'zero' => 0, 'places' => 2, 'thousands' => ',', 'decimals' => '.', 'negative' => '()', 'escape' => true, - 'fractionExponent' => 2 - ), - 'JPY' => array( - 'wholeSymbol' => '¥', 'wholePosition' => 'before', 'fractionSymbol' => false, 'fractionPosition' => 'after', - 'zero' => 0, 'places' => 2, 'thousands' => ',', 'decimals' => '.', 'negative' => '()', 'escape' => true, - 'fractionExponent' => 0 - ), - ); - -/** - * Default options for currency formats - * - * @var array - */ - protected static $_currencyDefaults = array( - 'wholeSymbol' => '', 'wholePosition' => 'before', 'fractionSymbol' => false, 'fractionPosition' => 'after', - 'zero' => '0', 'places' => 2, 'thousands' => ',', 'decimals' => '.', 'negative' => '()', 'escape' => true, - 'fractionExponent' => 2 - ); - -/** - * Default currency used by CakeNumber::currency() - * - * @var string - */ - protected static $_defaultCurrency = 'USD'; - -/** - * If native number_format() should be used. If >= PHP5.4 - * - * @var bool - */ - protected static $_numberFormatSupport = null; - -/** - * Formats a number with a level of precision. - * - * @param float $value A floating point number. - * @param int $precision The precision of the returned number. - * @return float Formatted float. - * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/number.html#NumberHelper::precision - */ - public static function precision($value, $precision = 3) { - return sprintf("%01.{$precision}f", $value); - } - -/** - * Returns a formatted-for-humans file size. - * - * @param int $size Size in bytes - * @return string Human readable size - * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/number.html#NumberHelper::toReadableSize - */ - public static function toReadableSize($size) { - switch (true) { - case $size < 1024: - return __dn('cake', '%d Byte', '%d Bytes', $size, $size); - case round($size / 1024) < 1024: - return __d('cake', '%s KB', static::precision($size / 1024, 0)); - case round($size / 1024 / 1024, 2) < 1024: - return __d('cake', '%s MB', static::precision($size / 1024 / 1024, 2)); - case round($size / 1024 / 1024 / 1024, 2) < 1024: - return __d('cake', '%s GB', static::precision($size / 1024 / 1024 / 1024, 2)); - default: - return __d('cake', '%s TB', static::precision($size / 1024 / 1024 / 1024 / 1024, 2)); - } - } - -/** - * Converts filesize from human readable string to bytes - * - * @param string $size Size in human readable string like '5MB', '5M', '500B', '50kb' etc. - * @param mixed $default Value to be returned when invalid size was used, for example 'Unknown type' - * @return mixed Number of bytes as integer on success, `$default` on failure if not false - * @throws CakeException On invalid Unit type. - * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/number.html#NumberHelper::fromReadableSize - */ - public static function fromReadableSize($size, $default = false) { - if (ctype_digit($size)) { - return (int)$size; - } - $size = strtoupper($size); - - $l = -2; - $i = array_search(substr($size, -2), array('KB', 'MB', 'GB', 'TB', 'PB')); - if ($i === false) { - $l = -1; - $i = array_search(substr($size, -1), array('K', 'M', 'G', 'T', 'P')); - } - if ($i !== false) { - $size = substr($size, 0, $l); - return $size * pow(1024, $i + 1); - } - - if (substr($size, -1) === 'B' && ctype_digit(substr($size, 0, -1))) { - $size = substr($size, 0, -1); - return (int)$size; - } - - if ($default !== false) { - return $default; - } - throw new CakeException(__d('cake_dev', 'No unit type.')); - } - -/** - * Formats a number into a percentage string. - * - * Options: - * - * - `multiply`: Multiply the input value by 100 for decimal percentages. - * - * @param float $value A floating point number - * @param int $precision The precision of the returned number - * @param array $options Options - * @return string Percentage string - * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/number.html#NumberHelper::toPercentage - */ - public static function toPercentage($value, $precision = 2, $options = array()) { - $options += array('multiply' => false); - if ($options['multiply']) { - $value *= 100; - } - return static::precision($value, $precision) . '%'; - } - -/** - * Formats a number into a currency format. - * - * @param float $value A floating point number - * @param int $options If integer then places, if string then before, if (,.-) then use it - * or array with places and before keys - * @return string formatted number - * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/number.html#NumberHelper::format - */ - public static function format($value, $options = false) { - $places = 0; - if (is_int($options)) { - $places = $options; - } - - $separators = array(',', '.', '-', ':'); - - $before = $after = null; - if (is_string($options) && !in_array($options, $separators)) { - $before = $options; - } - $thousands = ','; - if (!is_array($options) && in_array($options, $separators)) { - $thousands = $options; - } - $decimals = '.'; - if (!is_array($options) && in_array($options, $separators)) { - $decimals = $options; - } - - $escape = true; - if (is_array($options)) { - $defaults = array('before' => '$', 'places' => 2, 'thousands' => ',', 'decimals' => '.'); - $options += $defaults; - extract($options); - } - - $value = static::_numberFormat($value, $places, '.', ''); - $out = $before . static::_numberFormat($value, $places, $decimals, $thousands) . $after; - - if ($escape) { - return h($out); - } - return $out; - } - -/** - * Formats a number into a currency format to show deltas (signed differences in value). - * - * ### Options - * - * - `places` - Number of decimal places to use. ie. 2 - * - `fractionExponent` - Fraction exponent of this specific currency. Defaults to 2. - * - `before` - The string to place before whole numbers. ie. '[' - * - `after` - The string to place after decimal numbers. ie. ']' - * - `thousands` - Thousands separator ie. ',' - * - `decimals` - Decimal separator symbol ie. '.' - * - * @param float $value A floating point number - * @param array $options Options list. - * @return string formatted delta - * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/number.html#NumberHelper::formatDelta - */ - public static function formatDelta($value, $options = array()) { - $places = isset($options['places']) ? $options['places'] : 0; - $value = static::_numberFormat($value, $places, '.', ''); - $sign = $value > 0 ? '+' : ''; - $options['before'] = isset($options['before']) ? $options['before'] . $sign : $sign; - return static::format($value, $options); - } - -/** - * Alternative number_format() to accommodate multibyte decimals and thousands < PHP 5.4 - * - * @param float $value Value to format. - * @param int $places Decimal places to use. - * @param string $decimals Decimal position string. - * @param string $thousands Thousands separator string. - * @return string - */ - protected static function _numberFormat($value, $places = 0, $decimals = '.', $thousands = ',') { - if (!isset(static::$_numberFormatSupport)) { - static::$_numberFormatSupport = version_compare(PHP_VERSION, '5.4.0', '>='); - } - if (static::$_numberFormatSupport) { - return number_format($value, $places, $decimals, $thousands); - } - $value = number_format($value, $places, '.', ''); - $after = ''; - $foundDecimal = strpos($value, '.'); - if ($foundDecimal !== false) { - $after = substr($value, $foundDecimal); - $value = substr($value, 0, $foundDecimal); - } - while (($foundThousand = preg_replace('/(\d+)(\d\d\d)/', '\1 \2', $value)) !== $value) { - $value = $foundThousand; - } - $value .= $after; - return strtr($value, array(' ' => $thousands, '.' => $decimals)); - } - -/** - * Formats a number into a currency format. - * - * ### Options - * - * - `wholeSymbol` - The currency symbol to use for whole numbers, - * greater than 1, or less than -1. - * - `wholePosition` - The position the whole symbol should be placed - * valid options are 'before' & 'after'. - * - `fractionSymbol` - The currency symbol to use for fractional numbers. - * - `fractionPosition` - The position the fraction symbol should be placed - * valid options are 'before' & 'after'. - * - `before` - The currency symbol to place before whole numbers - * ie. '$'. `before` is an alias for `wholeSymbol`. - * - `after` - The currency symbol to place after decimal numbers - * ie. 'c'. Set to boolean false to use no decimal symbol. - * eg. 0.35 => $0.35. `after` is an alias for `fractionSymbol` - * - `zero` - The text to use for zero values, can be a - * string or a number. ie. 0, 'Free!' - * - `places` - Number of decimal places to use. ie. 2 - * - `fractionExponent` - Fraction exponent of this specific currency. Defaults to 2. - * - `thousands` - Thousands separator ie. ',' - * - `decimals` - Decimal separator symbol ie. '.' - * - `negative` - Symbol for negative numbers. If equal to '()', - * the number will be wrapped with ( and ) - * - `escape` - Should the output be escaped for html special characters. - * The default value for this option is controlled by the currency settings. - * By default all currencies contain utf-8 symbols and don't need this changed. If you require - * non HTML encoded symbols you will need to update the settings with the correct bytes. - * - * @param float $value Value to format. - * @param string $currency Shortcut to default options. Valid values are - * 'USD', 'EUR', 'GBP', otherwise set at least 'before' and 'after' options. - * @param array $options Options list. - * @return string Number formatted as a currency. - * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/number.html#NumberHelper::currency - */ - public static function currency($value, $currency = null, $options = array()) { - $defaults = static::$_currencyDefaults; - if ($currency === null) { - $currency = static::defaultCurrency(); - } - - if (isset(static::$_currencies[$currency])) { - $defaults = static::$_currencies[$currency]; - } elseif (is_string($currency)) { - $options['before'] = $currency; - } - - $options += $defaults; - - if (isset($options['before']) && $options['before'] !== '') { - $options['wholeSymbol'] = $options['before']; - } - if (isset($options['after']) && !$options['after'] !== '') { - $options['fractionSymbol'] = $options['after']; - } - - $result = $options['before'] = $options['after'] = null; - - $symbolKey = 'whole'; - $value = (float)$value; - if (!$value) { - if ($options['zero'] !== 0) { - return $options['zero']; - } - } elseif ($value < 1 && $value > -1) { - if ($options['fractionSymbol'] !== false) { - $multiply = pow(10, $options['fractionExponent']); - $value = $value * $multiply; - $options['places'] = null; - $symbolKey = 'fraction'; - } - } - - $position = $options[$symbolKey . 'Position'] !== 'after' ? 'before' : 'after'; - $options[$position] = $options[$symbolKey . 'Symbol']; - - $abs = abs($value); - $result = static::format($abs, $options); - - if ($value < 0) { - if ($options['negative'] === '()') { - $result = '(' . $result . ')'; - } else { - $result = $options['negative'] . $result; - } - } - return $result; - } - -/** - * Add a currency format to the Number helper. Makes reusing - * currency formats easier. - * - * ``` $number->addFormat('NOK', array('before' => 'Kr. ')); ``` - * - * You can now use `NOK` as a shortform when formatting currency amounts. - * - * ``` $number->currency($value, 'NOK'); ``` - * - * Added formats are merged with the defaults defined in CakeNumber::$_currencyDefaults - * See CakeNumber::currency() for more information on the various options and their function. - * - * @param string $formatName The format name to be used in the future. - * @param array $options The array of options for this format. - * @return void - * @see NumberHelper::currency() - * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/number.html#NumberHelper::addFormat - */ - public static function addFormat($formatName, $options) { - static::$_currencies[$formatName] = $options + static::$_currencyDefaults; - } - -/** - * Getter/setter for default currency - * - * @param string $currency Default currency string used by currency() if $currency argument is not provided - * @return string Currency - * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/number.html#NumberHelper::defaultCurrency - */ - public static function defaultCurrency($currency = null) { - if ($currency) { - static::$_defaultCurrency = $currency; - } - return static::$_defaultCurrency; - } +class CakeNumber +{ + + /** + * Currencies supported by the helper. You can add additional currency formats + * with CakeNumber::addFormat + * + * @var array + */ + protected static $_currencies = [ + 'AUD' => [ + 'wholeSymbol' => '$', 'wholePosition' => 'before', 'fractionSymbol' => 'c', 'fractionPosition' => 'after', + 'zero' => 0, 'places' => 2, 'thousands' => ',', 'decimals' => '.', 'negative' => '()', 'escape' => true, + 'fractionExponent' => 2 + ], + 'CAD' => [ + 'wholeSymbol' => '$', 'wholePosition' => 'before', 'fractionSymbol' => 'c', 'fractionPosition' => 'after', + 'zero' => 0, 'places' => 2, 'thousands' => ',', 'decimals' => '.', 'negative' => '()', 'escape' => true, + 'fractionExponent' => 2 + ], + 'USD' => [ + 'wholeSymbol' => '$', 'wholePosition' => 'before', 'fractionSymbol' => 'c', 'fractionPosition' => 'after', + 'zero' => 0, 'places' => 2, 'thousands' => ',', 'decimals' => '.', 'negative' => '()', 'escape' => true, + 'fractionExponent' => 2 + ], + 'EUR' => [ + 'wholeSymbol' => '€', 'wholePosition' => 'before', 'fractionSymbol' => false, 'fractionPosition' => 'after', + 'zero' => 0, 'places' => 2, 'thousands' => '.', 'decimals' => ',', 'negative' => '()', 'escape' => true, + 'fractionExponent' => 0 + ], + 'GBP' => [ + 'wholeSymbol' => '£', 'wholePosition' => 'before', 'fractionSymbol' => 'p', 'fractionPosition' => 'after', + 'zero' => 0, 'places' => 2, 'thousands' => ',', 'decimals' => '.', 'negative' => '()', 'escape' => true, + 'fractionExponent' => 2 + ], + 'JPY' => [ + 'wholeSymbol' => '¥', 'wholePosition' => 'before', 'fractionSymbol' => false, 'fractionPosition' => 'after', + 'zero' => 0, 'places' => 2, 'thousands' => ',', 'decimals' => '.', 'negative' => '()', 'escape' => true, + 'fractionExponent' => 0 + ], + ]; + + /** + * Default options for currency formats + * + * @var array + */ + protected static $_currencyDefaults = [ + 'wholeSymbol' => '', 'wholePosition' => 'before', 'fractionSymbol' => false, 'fractionPosition' => 'after', + 'zero' => '0', 'places' => 2, 'thousands' => ',', 'decimals' => '.', 'negative' => '()', 'escape' => true, + 'fractionExponent' => 2 + ]; + + /** + * Default currency used by CakeNumber::currency() + * + * @var string + */ + protected static $_defaultCurrency = 'USD'; + + /** + * If native number_format() should be used. If >= PHP5.4 + * + * @var bool + */ + protected static $_numberFormatSupport = null; + + /** + * Returns a formatted-for-humans file size. + * + * @param int $size Size in bytes + * @return string Human readable size + * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/number.html#NumberHelper::toReadableSize + */ + public static function toReadableSize($size) + { + switch (true) { + case $size < 1024: + return __dn('cake', '%d Byte', '%d Bytes', $size, $size); + case round($size / 1024) < 1024: + return __d('cake', '%s KB', static::precision($size / 1024, 0)); + case round($size / 1024 / 1024, 2) < 1024: + return __d('cake', '%s MB', static::precision($size / 1024 / 1024, 2)); + case round($size / 1024 / 1024 / 1024, 2) < 1024: + return __d('cake', '%s GB', static::precision($size / 1024 / 1024 / 1024, 2)); + default: + return __d('cake', '%s TB', static::precision($size / 1024 / 1024 / 1024 / 1024, 2)); + } + } + + /** + * Formats a number with a level of precision. + * + * @param float $value A floating point number. + * @param int $precision The precision of the returned number. + * @return float Formatted float. + * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/number.html#NumberHelper::precision + */ + public static function precision($value, $precision = 3) + { + return sprintf("%01.{$precision}f", $value); + } + + /** + * Converts filesize from human readable string to bytes + * + * @param string $size Size in human readable string like '5MB', '5M', '500B', '50kb' etc. + * @param mixed $default Value to be returned when invalid size was used, for example 'Unknown type' + * @return mixed Number of bytes as integer on success, `$default` on failure if not false + * @throws CakeException On invalid Unit type. + * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/number.html#NumberHelper::fromReadableSize + */ + public static function fromReadableSize($size, $default = false) + { + if (ctype_digit($size)) { + return (int)$size; + } + $size = strtoupper($size); + + $l = -2; + $i = array_search(substr($size, -2), ['KB', 'MB', 'GB', 'TB', 'PB']); + if ($i === false) { + $l = -1; + $i = array_search(substr($size, -1), ['K', 'M', 'G', 'T', 'P']); + } + if ($i !== false) { + $size = substr($size, 0, $l); + return $size * pow(1024, $i + 1); + } + + if (substr($size, -1) === 'B' && ctype_digit(substr($size, 0, -1))) { + $size = substr($size, 0, -1); + return (int)$size; + } + + if ($default !== false) { + return $default; + } + throw new CakeException(__d('cake_dev', 'No unit type.')); + } + + /** + * Formats a number into a percentage string. + * + * Options: + * + * - `multiply`: Multiply the input value by 100 for decimal percentages. + * + * @param float $value A floating point number + * @param int $precision The precision of the returned number + * @param array $options Options + * @return string Percentage string + * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/number.html#NumberHelper::toPercentage + */ + public static function toPercentage($value, $precision = 2, $options = []) + { + $options += ['multiply' => false]; + if ($options['multiply']) { + $value *= 100; + } + return static::precision($value, $precision) . '%'; + } + + /** + * Formats a number into a currency format to show deltas (signed differences in value). + * + * ### Options + * + * - `places` - Number of decimal places to use. ie. 2 + * - `fractionExponent` - Fraction exponent of this specific currency. Defaults to 2. + * - `before` - The string to place before whole numbers. ie. '[' + * - `after` - The string to place after decimal numbers. ie. ']' + * - `thousands` - Thousands separator ie. ',' + * - `decimals` - Decimal separator symbol ie. '.' + * + * @param float $value A floating point number + * @param array $options Options list. + * @return string formatted delta + * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/number.html#NumberHelper::formatDelta + */ + public static function formatDelta($value, $options = []) + { + $places = isset($options['places']) ? $options['places'] : 0; + $value = static::_numberFormat($value, $places, '.', ''); + $sign = $value > 0 ? '+' : ''; + $options['before'] = isset($options['before']) ? $options['before'] . $sign : $sign; + return static::format($value, $options); + } + + /** + * Alternative number_format() to accommodate multibyte decimals and thousands < PHP 5.4 + * + * @param float $value Value to format. + * @param int $places Decimal places to use. + * @param string $decimals Decimal position string. + * @param string $thousands Thousands separator string. + * @return string + */ + protected static function _numberFormat($value, $places = 0, $decimals = '.', $thousands = ',') + { + if (!isset(static::$_numberFormatSupport)) { + static::$_numberFormatSupport = version_compare(PHP_VERSION, '5.4.0', '>='); + } + if (static::$_numberFormatSupport) { + return number_format($value, $places, $decimals, $thousands); + } + $value = number_format($value, $places, '.', ''); + $after = ''; + $foundDecimal = strpos($value, '.'); + if ($foundDecimal !== false) { + $after = substr($value, $foundDecimal); + $value = substr($value, 0, $foundDecimal); + } + while (($foundThousand = preg_replace('/(\d+)(\d\d\d)/', '\1 \2', $value)) !== $value) { + $value = $foundThousand; + } + $value .= $after; + return strtr($value, [' ' => $thousands, '.' => $decimals]); + } + + /** + * Formats a number into a currency format. + * + * @param float $value A floating point number + * @param int $options If integer then places, if string then before, if (,.-) then use it + * or array with places and before keys + * @return string formatted number + * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/number.html#NumberHelper::format + */ + public static function format($value, $options = false) + { + $places = 0; + if (is_int($options)) { + $places = $options; + } + + $separators = [',', '.', '-', ':']; + + $before = $after = null; + if (is_string($options) && !in_array($options, $separators)) { + $before = $options; + } + $thousands = ','; + if (!is_array($options) && in_array($options, $separators)) { + $thousands = $options; + } + $decimals = '.'; + if (!is_array($options) && in_array($options, $separators)) { + $decimals = $options; + } + + $escape = true; + if (is_array($options)) { + $defaults = ['before' => '$', 'places' => 2, 'thousands' => ',', 'decimals' => '.']; + $options += $defaults; + extract($options); + } + + $value = static::_numberFormat($value, $places, '.', ''); + $out = $before . static::_numberFormat($value, $places, $decimals, $thousands) . $after; + + if ($escape) { + return h($out); + } + return $out; + } + + /** + * Formats a number into a currency format. + * + * ### Options + * + * - `wholeSymbol` - The currency symbol to use for whole numbers, + * greater than 1, or less than -1. + * - `wholePosition` - The position the whole symbol should be placed + * valid options are 'before' & 'after'. + * - `fractionSymbol` - The currency symbol to use for fractional numbers. + * - `fractionPosition` - The position the fraction symbol should be placed + * valid options are 'before' & 'after'. + * - `before` - The currency symbol to place before whole numbers + * ie. '$'. `before` is an alias for `wholeSymbol`. + * - `after` - The currency symbol to place after decimal numbers + * ie. 'c'. Set to boolean false to use no decimal symbol. + * eg. 0.35 => $0.35. `after` is an alias for `fractionSymbol` + * - `zero` - The text to use for zero values, can be a + * string or a number. ie. 0, 'Free!' + * - `places` - Number of decimal places to use. ie. 2 + * - `fractionExponent` - Fraction exponent of this specific currency. Defaults to 2. + * - `thousands` - Thousands separator ie. ',' + * - `decimals` - Decimal separator symbol ie. '.' + * - `negative` - Symbol for negative numbers. If equal to '()', + * the number will be wrapped with ( and ) + * - `escape` - Should the output be escaped for html special characters. + * The default value for this option is controlled by the currency settings. + * By default all currencies contain utf-8 symbols and don't need this changed. If you require + * non HTML encoded symbols you will need to update the settings with the correct bytes. + * + * @param float $value Value to format. + * @param string $currency Shortcut to default options. Valid values are + * 'USD', 'EUR', 'GBP', otherwise set at least 'before' and 'after' options. + * @param array $options Options list. + * @return string Number formatted as a currency. + * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/number.html#NumberHelper::currency + */ + public static function currency($value, $currency = null, $options = []) + { + $defaults = static::$_currencyDefaults; + if ($currency === null) { + $currency = static::defaultCurrency(); + } + + if (isset(static::$_currencies[$currency])) { + $defaults = static::$_currencies[$currency]; + } else if (is_string($currency)) { + $options['before'] = $currency; + } + + $options += $defaults; + + if (isset($options['before']) && $options['before'] !== '') { + $options['wholeSymbol'] = $options['before']; + } + if (isset($options['after']) && !$options['after'] !== '') { + $options['fractionSymbol'] = $options['after']; + } + + $result = $options['before'] = $options['after'] = null; + + $symbolKey = 'whole'; + $value = (float)$value; + if (!$value) { + if ($options['zero'] !== 0) { + return $options['zero']; + } + } else if ($value < 1 && $value > -1) { + if ($options['fractionSymbol'] !== false) { + $multiply = pow(10, $options['fractionExponent']); + $value = $value * $multiply; + $options['places'] = null; + $symbolKey = 'fraction'; + } + } + + $position = $options[$symbolKey . 'Position'] !== 'after' ? 'before' : 'after'; + $options[$position] = $options[$symbolKey . 'Symbol']; + + $abs = abs($value); + $result = static::format($abs, $options); + + if ($value < 0) { + if ($options['negative'] === '()') { + $result = '(' . $result . ')'; + } else { + $result = $options['negative'] . $result; + } + } + return $result; + } + + /** + * Getter/setter for default currency + * + * @param string $currency Default currency string used by currency() if $currency argument is not provided + * @return string Currency + * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/number.html#NumberHelper::defaultCurrency + */ + public static function defaultCurrency($currency = null) + { + if ($currency) { + static::$_defaultCurrency = $currency; + } + return static::$_defaultCurrency; + } + + /** + * Add a currency format to the Number helper. Makes reusing + * currency formats easier. + * + * ``` $number->addFormat('NOK', array('before' => 'Kr. ')); ``` + * + * You can now use `NOK` as a shortform when formatting currency amounts. + * + * ``` $number->currency($value, 'NOK'); ``` + * + * Added formats are merged with the defaults defined in CakeNumber::$_currencyDefaults + * See CakeNumber::currency() for more information on the various options and their function. + * + * @param string $formatName The format name to be used in the future. + * @param array $options The array of options for this format. + * @return void + * @see NumberHelper::currency() + * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/number.html#NumberHelper::addFormat + */ + public static function addFormat($formatName, $options) + { + static::$_currencies[$formatName] = $options + static::$_currencyDefaults; + } } diff --git a/lib/Cake/Utility/CakeText.php b/lib/Cake/Utility/CakeText.php index 9cebf2f0..95c38cc8 100644 --- a/lib/Cake/Utility/CakeText.php +++ b/lib/Cake/Utility/CakeText.php @@ -21,7 +21,8 @@ * * @package Cake.Utility */ -class CakeText { +class CakeText +{ /** * Generate a random UUID @@ -29,7 +30,8 @@ class CakeText { * @see http://www.ietf.org/rfc/rfc4122.txt * @return string RFC 4122 UUID */ - public static function uuid() { + public static function uuid() + { $random = function_exists('random_int') ? 'random_int' : 'mt_rand'; return sprintf( '%04x%04x-%04x-%04x-%04x-%04x%04x%04x', @@ -61,25 +63,26 @@ public static function uuid() { * @param string $rightBound The right boundary to ignore separators in. * @return mixed Array of tokens in $data or original input if empty. */ - public static function tokenize($data, $separator = ',', $leftBound = '(', $rightBound = ')') { + public static function tokenize($data, $separator = ',', $leftBound = '(', $rightBound = ')') + { if (empty($data)) { - return array(); + return []; } $depth = 0; $offset = 0; $buffer = ''; - $results = array(); + $results = []; $length = mb_strlen($data); $open = false; while ($offset <= $length) { $tmpOffset = -1; - $offsets = array( + $offsets = [ mb_strpos($data, $separator, $offset), mb_strpos($data, $leftBound, $offset), mb_strpos($data, $rightBound, $offset) - ); + ]; for ($i = 0; $i < 3; $i++) { if ($offsets[$i] !== false && ($offsets[$i] < $tmpOffset || $tmpOffset == -1)) { $tmpOffset = $offsets[$i]; @@ -125,7 +128,7 @@ public static function tokenize($data, $separator = ',', $leftBound = '(', $righ return array_map('trim', $results); } - return array(); + return []; } /** @@ -149,10 +152,11 @@ public static function tokenize($data, $separator = ',', $leftBound = '(', $righ * @param array $options An array of options, see description above * @return string */ - public static function insert($str, $data, $options = array()) { - $defaults = array( + public static function insert($str, $data, $options = []) + { + $defaults = [ 'before' => ':', 'after' => null, 'escape' => '\\', 'format' => null, 'clean' => false - ); + ]; $options += $defaults; $format = $options['format']; $data = (array)$data; @@ -213,24 +217,25 @@ public static function insert($str, $data, $options = array()) { * @return string * @see CakeText::insert() */ - public static function cleanInsert($str, $options) { + public static function cleanInsert($str, $options) + { $clean = $options['clean']; if (!$clean) { return $str; } if ($clean === true) { - $clean = array('method' => 'text'); + $clean = ['method' => 'text']; } if (!is_array($clean)) { - $clean = array('method' => $options['clean']); + $clean = ['method' => $options['clean']]; } switch ($clean['method']) { case 'html': - $clean = array_merge(array( + $clean = array_merge([ 'word' => '[\w,.]+', 'andText' => true, 'replacement' => '', - ), $clean); + ], $clean); $kleenex = sprintf( '/[\s]*[a-z]+=(")(%s%s%s[\s]*)+\\1/i', preg_quote($options['before'], '/'), @@ -239,16 +244,16 @@ public static function cleanInsert($str, $options) { ); $str = preg_replace($kleenex, $clean['replacement'], $str); if ($clean['andText']) { - $options['clean'] = array('method' => 'text'); + $options['clean'] = ['method' => 'text']; $str = CakeText::cleanInsert($str, $options); } break; case 'text': - $clean = array_merge(array( + $clean = array_merge([ 'word' => '[\w,.]+', 'gap' => '[\s]*(?:(?:and|or)[\s]*)?', 'replacement' => '', - ), $clean); + ], $clean); $kleenex = sprintf( '/(%s%s%s%s|%s%s%s%s)/', @@ -281,11 +286,12 @@ public static function cleanInsert($str, $options) { * @param array|int $options Array of options to use, or an integer to wrap the text to. * @return string Formatted text. */ - public static function wrap($text, $options = array()) { + public static function wrap($text, $options = []) + { if (is_numeric($options)) { - $options = array('width' => $options); + $options = ['width' => $options]; } - $options += array('width' => 72, 'wordWrap' => true, 'indent' => null, 'indentAt' => 0); + $options += ['width' => 72, 'wordWrap' => true, 'indent' => null, 'indentAt' => 0]; if ($options['wordWrap']) { $wrapped = static::wordWrap($text, $options['width'], "\n"); } else { @@ -310,7 +316,8 @@ public static function wrap($text, $options = array()) { * @param bool $cut If the cut is set to true, the string is always wrapped at the specified width. * @return string Formatted text. */ - public static function wordWrap($text, $width = 72, $break = "\n", $cut = false) { + public static function wordWrap($text, $width = 72, $break = "\n", $cut = false) + { $paragraphs = explode($break, $text); foreach ($paragraphs as &$paragraph) { $paragraph = static::_wordWrap($paragraph, $width, $break, $cut); @@ -327,9 +334,10 @@ public static function wordWrap($text, $width = 72, $break = "\n", $cut = false) * @param bool $cut If the cut is set to true, the string is always wrapped at the specified width. * @return string Formatted text. */ - protected static function _wordWrap($text, $width = 72, $break = "\n", $cut = false) { + protected static function _wordWrap($text, $width = 72, $break = "\n", $cut = false) + { if ($cut) { - $parts = array(); + $parts = []; while (mb_strlen($text) > 0) { $part = mb_substr($text, 0, $width); $parts[] = trim($part); @@ -338,7 +346,7 @@ protected static function _wordWrap($text, $width = 72, $break = "\n", $cut = fa return implode($break, $parts); } - $parts = array(); + $parts = []; while (mb_strlen($text) > 0) { if ($width >= mb_strlen($text)) { $parts[] = trim($text); @@ -383,22 +391,23 @@ protected static function _wordWrap($text, $width = 72, $break = "\n", $cut = fa * @return string The highlighted text * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/text.html#TextHelper::highlight */ - public static function highlight($text, $phrase, $options = array()) { + public static function highlight($text, $phrase, $options = []) + { if (empty($phrase)) { return $text; } - $defaults = array( + $defaults = [ 'format' => '\1', 'html' => false, 'regex' => "|%s|iu" - ); + ]; $options += $defaults; extract($options); if (is_array($phrase)) { - $replace = array(); - $with = array(); + $replace = []; + $with = []; foreach ($phrase as $key => $segment) { $segment = '(' . preg_quote($segment, '|') . ')'; @@ -428,7 +437,8 @@ public static function highlight($text, $phrase, $options = array()) { * @return string The text without links * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/text.html#TextHelper::stripLinks */ - public static function stripLinks($text) { + public static function stripLinks($text) + { return preg_replace('|]+>|im', '', preg_replace('|<\/a>|im', '', $text)); } @@ -448,10 +458,11 @@ public static function stripLinks($text) { * @param array $options An array of options. * @return string Trimmed string. */ - public static function tail($text, $length = 100, $options = array()) { - $defaults = array( + public static function tail($text, $length = 100, $options = []) + { + $defaults = [ 'ellipsis' => '...', 'exact' => true - ); + ]; $options += $defaults; extract($options); @@ -472,6 +483,51 @@ class_exists('Multibyte'); return $ellipsis . $truncate; } + /** + * Extracts an excerpt from the text surrounding the phrase with a number of characters on each side + * determined by radius. + * + * @param string $text CakeText to search the phrase in + * @param string $phrase Phrase that will be searched for + * @param int $radius The amount of characters that will be returned on each side of the founded phrase + * @param string $ellipsis Ending that will be appended + * @return string Modified string + * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/text.html#TextHelper::excerpt + */ + public static function excerpt($text, $phrase, $radius = 100, $ellipsis = '...') + { + if (empty($text) || empty($phrase)) { + return static::truncate($text, $radius * 2, ['ellipsis' => $ellipsis]); + } + + $append = $prepend = $ellipsis; + + $phraseLen = mb_strlen($phrase); + $textLen = mb_strlen($text); + + $pos = mb_strpos(mb_strtolower($text), mb_strtolower($phrase)); + if ($pos === false) { + return mb_substr($text, 0, $radius) . $ellipsis; + } + + $startPos = $pos - $radius; + if ($startPos <= 0) { + $startPos = 0; + $prepend = ''; + } + + $endPos = $pos + $phraseLen + $radius; + if ($endPos >= $textLen) { + $endPos = $textLen; + $append = ''; + } + + $excerpt = mb_substr($text, $startPos, $endPos - $startPos); + $excerpt = $prepend . $excerpt . $append; + + return $excerpt; + } + /** * Truncates text. * @@ -490,13 +546,14 @@ class_exists('Multibyte'); * @return string Trimmed string. * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/text.html#TextHelper::truncate */ - public static function truncate($text, $length = 100, $options = array()) { - $defaults = array( + public static function truncate($text, $length = 100, $options = []) + { + $defaults = [ 'ellipsis' => '...', 'exact' => true, 'html' => false - ); + ]; if (isset($options['ending'])) { $defaults['ellipsis'] = $options['ending']; - } elseif (!empty($options['html']) && Configure::read('App.encoding') === 'UTF-8') { + } else if (!empty($options['html']) && Configure::read('App.encoding') === 'UTF-8') { $defaults['ellipsis'] = "\xe2\x80\xa6"; } $options += $defaults; @@ -511,7 +568,7 @@ class_exists('Multibyte'); return $text; } $totalLength = mb_strlen(strip_tags($ellipsis)); - $openTags = array(); + $openTags = []; $truncate = ''; preg_match_all('/(<\/?([\w+]+)[^>]*>)?([^<>]*)/', $text, $tags, PREG_SET_ORDER); @@ -519,7 +576,7 @@ class_exists('Multibyte'); if (!preg_match('/img|br|input|hr|area|base|basefont|col|frame|isindex|link|meta|param/s', $tag[2])) { if (preg_match('/<[\w]+[^>]*>/s', $tag[0])) { array_unshift($openTags, $tag[2]); - } elseif (preg_match('/<\/([\w]+)[^>]*>/s', $tag[0], $closeTag)) { + } else if (preg_match('/<\/([\w]+)[^>]*>/s', $tag[0], $closeTag)) { $pos = array_search($closeTag[1], $openTags); if ($pos !== false) { array_splice($openTags, $pos, 1); @@ -599,50 +656,6 @@ class_exists('Multibyte'); return $truncate; } - /** - * Extracts an excerpt from the text surrounding the phrase with a number of characters on each side - * determined by radius. - * - * @param string $text CakeText to search the phrase in - * @param string $phrase Phrase that will be searched for - * @param int $radius The amount of characters that will be returned on each side of the founded phrase - * @param string $ellipsis Ending that will be appended - * @return string Modified string - * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/text.html#TextHelper::excerpt - */ - public static function excerpt($text, $phrase, $radius = 100, $ellipsis = '...') { - if (empty($text) || empty($phrase)) { - return static::truncate($text, $radius * 2, array('ellipsis' => $ellipsis)); - } - - $append = $prepend = $ellipsis; - - $phraseLen = mb_strlen($phrase); - $textLen = mb_strlen($text); - - $pos = mb_strpos(mb_strtolower($text), mb_strtolower($phrase)); - if ($pos === false) { - return mb_substr($text, 0, $radius) . $ellipsis; - } - - $startPos = $pos - $radius; - if ($startPos <= 0) { - $startPos = 0; - $prepend = ''; - } - - $endPos = $pos + $phraseLen + $radius; - if ($endPos >= $textLen) { - $endPos = $textLen; - $append = ''; - } - - $excerpt = mb_substr($text, $startPos, $endPos - $startPos); - $excerpt = $prepend . $excerpt . $append; - - return $excerpt; - } - /** * Creates a comma separated list where the last two items are joined with 'and', forming natural language. * @@ -652,7 +665,8 @@ public static function excerpt($text, $phrase, $radius = 100, $ellipsis = '...') * @return string The glued together string. * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/text.html#TextHelper::toList */ - public static function toList($list, $and = null, $separator = ', ') { + public static function toList($list, $and = null, $separator = ', ') + { if ($and === null) { $and = __d('cake', 'and'); } diff --git a/lib/Cake/Utility/CakeTime.php b/lib/Cake/Utility/CakeTime.php index 63911bfd..061c3d85 100755 --- a/lib/Cake/Utility/CakeTime.php +++ b/lib/Cake/Utility/CakeTime.php @@ -26,1185 +26,1220 @@ * @package Cake.Utility * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/time.html */ -class CakeTime { - -/** - * The format to use when formatting a time using `CakeTime::nice()` - * - * The format should use the locale strings as defined in the PHP docs under - * `strftime` (http://php.net/manual/en/function.strftime.php) - * - * @var string - * @see CakeTime::format() - */ - public static $niceFormat = '%a, %b %eS %Y, %H:%M'; - -/** - * The format to use when formatting a time using `CakeTime::timeAgoInWords()` - * and the difference is more than `CakeTime::$wordEnd` - * - * @var string - * @see CakeTime::timeAgoInWords() - */ - public static $wordFormat = 'j/n/y'; - -/** - * The format to use when formatting a time using `CakeTime::niceShort()` - * and the difference is between 3 and 7 days - * - * @var string - * @see CakeTime::niceShort() - */ - public static $niceShortFormat = '%B %d, %H:%M'; - -/** - * The format to use when formatting a time using `CakeTime::timeAgoInWords()` - * and the difference is less than `CakeTime::$wordEnd` - * - * @var array - * @see CakeTime::timeAgoInWords() - */ - public static $wordAccuracy = array( - 'year' => 'day', - 'month' => 'day', - 'week' => 'day', - 'day' => 'hour', - 'hour' => 'minute', - 'minute' => 'minute', - 'second' => 'second', - ); - -/** - * The end of relative time telling - * - * @var string - * @see CakeTime::timeAgoInWords() - */ - public static $wordEnd = '+1 month'; - -/** - * Temporary variable containing the timestamp value, used internally in convertSpecifiers() - * - * @var int - */ - protected static $_time = null; - -/** - * Magic set method for backwards compatibility. - * Used by TimeHelper to modify static variables in CakeTime - * - * @param string $name Variable name - * @param mixes $value Variable value - * @return void - */ - public function __set($name, $value) { - switch ($name) { - case 'niceFormat': - static::${$name} = $value; - break; - } - } - -/** - * Magic set method for backwards compatibility. - * Used by TimeHelper to get static variables in CakeTime - * - * @param string $name Variable name - * @return mixed - */ - public function __get($name) { - switch ($name) { - case 'niceFormat': - return static::${$name}; - default: - return null; - } - } - -/** - * Converts a string representing the format for the function strftime and returns a - * Windows safe and i18n aware format. - * - * @param string $format Format with specifiers for strftime function. - * Accepts the special specifier %S which mimics the modifier S for date() - * @param string $time UNIX timestamp - * @return string Windows safe and date() function compatible format for strftime - * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/time.html#TimeHelper::convertSpecifiers - */ - public static function convertSpecifiers($format, $time = null) { - if (!$time) { - $time = time(); - } - static::$_time = $time; - return preg_replace_callback('/\%(\w+)/', array('CakeTime', '_translateSpecifier'), $format); - } - -/** - * Auxiliary function to translate a matched specifier element from a regular expression into - * a Windows safe and i18n aware specifier - * - * @param array $specifier match from regular expression - * @return string converted element - */ - protected static function _translateSpecifier($specifier) { - switch ($specifier[1]) { - case 'a': - $abday = __dc('cake', 'abday', 5); - if (is_array($abday)) { - return $abday[date('w', static::$_time)]; - } - break; - case 'A': - $day = __dc('cake', 'day', 5); - if (is_array($day)) { - return $day[date('w', static::$_time)]; - } - break; - case 'c': - $format = __dc('cake', 'd_t_fmt', 5); - if ($format !== 'd_t_fmt') { - return static::convertSpecifiers($format, static::$_time); - } - break; - case 'C': - return sprintf("%02d", date('Y', static::$_time) / 100); - case 'D': - return '%m/%d/%y'; - case 'e': - if (DS === '/') { - return '%e'; - } - $day = date('j', static::$_time); - if ($day < 10) { - $day = ' ' . $day; - } - return $day; - case 'eS' : - return date('jS', static::$_time); - case 'b': - case 'h': - $months = __dc('cake', 'abmon', 5); - if (is_array($months)) { - return $months[date('n', static::$_time) - 1]; - } - return '%b'; - case 'B': - $months = __dc('cake', 'mon', 5); - if (is_array($months)) { - return $months[date('n', static::$_time) - 1]; - } - break; - case 'n': - return "\n"; - case 'p': - case 'P': - $default = array('am' => 0, 'pm' => 1); - $meridiem = $default[date('a', static::$_time)]; - $format = __dc('cake', 'am_pm', 5); - if (is_array($format)) { - $meridiem = $format[$meridiem]; - return ($specifier[1] === 'P') ? strtolower($meridiem) : strtoupper($meridiem); - } - break; - case 'r': - $complete = __dc('cake', 't_fmt_ampm', 5); - if ($complete !== 't_fmt_ampm') { - return str_replace('%p', static::_translateSpecifier(array('%p', 'p')), $complete); - } - break; - case 'R': - return date('H:i', static::$_time); - case 't': - return "\t"; - case 'T': - return '%H:%M:%S'; - case 'u': - return ($weekDay = date('w', static::$_time)) ? $weekDay : 7; - case 'x': - $format = __dc('cake', 'd_fmt', 5); - if ($format !== 'd_fmt') { - return static::convertSpecifiers($format, static::$_time); - } - break; - case 'X': - $format = __dc('cake', 't_fmt', 5); - if ($format !== 't_fmt') { - return static::convertSpecifiers($format, static::$_time); - } - break; - } - return $specifier[0]; - } - -/** - * Converts given time (in server's time zone) to user's local time, given his/her timezone. - * - * @param int $serverTime Server's timestamp. - * @param string|DateTimeZone $timezone User's timezone string or DateTimeZone object. - * @return int User's timezone timestamp. - * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/time.html#TimeHelper::convert - */ - public static function convert($serverTime, $timezone) { - static $serverTimezone = null; - if ($serverTimezone === null || (date_default_timezone_get() !== $serverTimezone->getName())) { - $serverTimezone = new DateTimeZone(date_default_timezone_get()); - } - $serverOffset = $serverTimezone->getOffset(new DateTime('@' . $serverTime)); - $gmtTime = $serverTime - $serverOffset; - if (is_numeric($timezone)) { - $userOffset = $timezone * (60 * 60); - } else { - $timezone = static::timezone($timezone); - $userOffset = $timezone->getOffset(new DateTime('@' . $gmtTime)); - } - $userTime = $gmtTime + $userOffset; - return (int)$userTime; - } - -/** - * Returns a timezone object from a string or the user's timezone object - * - * @param string|DateTimeZone $timezone Timezone string or DateTimeZone object - * If null it tries to get timezone from 'Config.timezone' config var - * @return DateTimeZone Timezone object - * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/time.html#TimeHelper::timezone - */ - public static function timezone($timezone = null) { - static $tz = null; - - if (is_object($timezone)) { - if ($tz === null || $tz->getName() !== $timezone->getName()) { - $tz = $timezone; - } - } else { - if ($timezone === null) { - $timezone = Configure::read('Config.timezone'); - if ($timezone === null) { - $timezone = date_default_timezone_get(); - } - } - - if ($tz === null || $tz->getName() !== $timezone) { - $tz = new DateTimeZone($timezone); - } - } - - return $tz; - } - -/** - * Returns server's offset from GMT in seconds. - * - * @return int Offset - * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/time.html#TimeHelper::serverOffset - */ - public static function serverOffset() { - return date('Z', time()); - } - -/** - * Returns a timestamp, given either a UNIX timestamp or a valid strtotime() date string. - * - * @param int|string|DateTime $dateString UNIX timestamp, strtotime() valid string or DateTime object - * @param string|DateTimeZone $timezone Timezone string or DateTimeZone object - * @return int|false Parsed given timezone timestamp. - * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/time.html#TimeHelper::fromString - */ - public static function fromString($dateString, $timezone = null) { - if (empty($dateString)) { - return false; - } - - $containsDummyDate = (is_string($dateString) && substr($dateString, 0, 10) === '0000-00-00'); - if ($containsDummyDate) { - return false; - } - - if (is_int($dateString) || is_numeric($dateString)) { - $date = (int)$dateString; - } elseif ($dateString instanceof DateTime && - $dateString->getTimezone()->getName() != date_default_timezone_get() - ) { - $clone = clone $dateString; - $clone->setTimezone(new DateTimeZone(date_default_timezone_get())); - $date = (int)$clone->format('U') + $clone->getOffset(); - } elseif ($dateString instanceof DateTime) { - $date = (int)$dateString->format('U'); - } else { - $date = strtotime($dateString); - } - - if ($date === -1 || empty($date)) { - return false; - } - - if ($timezone === null) { - $timezone = Configure::read('Config.timezone'); - } - - if ($timezone !== null) { - return static::convert($date, $timezone); - } - return $date; - } - -/** - * Returns a nicely formatted date string for given Datetime string. - * - * See http://php.net/manual/en/function.strftime.php for information on formatting - * using locale strings. - * - * @param int|string|DateTime $date UNIX timestamp, strtotime() valid string or DateTime object - * @param string|DateTimeZone $timezone Timezone string or DateTimeZone object - * @param string $format The format to use. If null, `CakeTime::$niceFormat` is used - * @return string Formatted date string - * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/time.html#TimeHelper::nice - */ - public static function nice($date = null, $timezone = null, $format = null) { - if (!$date) { - $date = time(); - } - $timestamp = static::fromString($date, $timezone); - if (!$format) { - $format = static::$niceFormat; - } - $convertedFormat = static::convertSpecifiers($format, $timestamp); - return static::_strftimeWithTimezone($convertedFormat, $timestamp, $date, $timezone); - } - -/** - * Returns a formatted descriptive date string for given datetime string. - * - * If the given date is today, the returned string could be "Today, 16:54". - * If the given date is tomorrow, the returned string could be "Tomorrow, 16:54". - * If the given date was yesterday, the returned string could be "Yesterday, 16:54". - * If the given date is within next or last week, the returned string could be "On Thursday, 16:54". - * If $dateString's year is the current year, the returned string does not - * include mention of the year. - * - * @param int|string|DateTime $date UNIX timestamp, strtotime() valid string or DateTime object - * @param string|DateTimeZone $timezone Timezone string or DateTimeZone object - * @return string Described, relative date string - * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/time.html#TimeHelper::niceShort - */ - public static function niceShort($date = null, $timezone = null) { - if (!$date) { - $date = time(); - } - $timestamp = static::fromString($date, $timezone); - - if (static::isToday($date, $timezone)) { - $formattedDate = static::_strftimeWithTimezone("%H:%M", $timestamp, $date, $timezone); - return __d('cake', 'Today, %s', $formattedDate); - } - if (static::wasYesterday($date, $timezone)) { - $formattedDate = static::_strftimeWithTimezone("%H:%M", $timestamp, $date, $timezone); - return __d('cake', 'Yesterday, %s', $formattedDate); - } - if (static::isTomorrow($date, $timezone)) { - $formattedDate = static::_strftimeWithTimezone("%H:%M", $timestamp, $date, $timezone); - return __d('cake', 'Tomorrow, %s', $formattedDate); - } - - $d = static::_strftimeWithTimezone("%w", $timestamp, $date, $timezone); - $day = array( - __d('cake', 'Sunday'), - __d('cake', 'Monday'), - __d('cake', 'Tuesday'), - __d('cake', 'Wednesday'), - __d('cake', 'Thursday'), - __d('cake', 'Friday'), - __d('cake', 'Saturday') - ); - if (static::wasWithinLast('7 days', $date, $timezone)) { - $formattedDate = static::_strftimeWithTimezone(static::$niceShortFormat, $timestamp, $date, $timezone); - return sprintf('%s %s', $day[$d], $formattedDate); - } - if (static::isWithinNext('7 days', $date, $timezone)) { - $formattedDate = static::_strftimeWithTimezone(static::$niceShortFormat, $timestamp, $date, $timezone); - return __d('cake', 'On %s %s', $day[$d], $formattedDate); - } - - $y = ''; - if (!static::isThisYear($timestamp)) { - $y = ' %Y'; - } - $format = static::convertSpecifiers("%b %eS{$y}, %H:%M", $timestamp); - return static::_strftimeWithTimezone($format, $timestamp, $date, $timezone); - } - -/** - * Returns a partial SQL string to search for all records between two dates. - * - * @param int|string|DateTime $begin UNIX timestamp, strtotime() valid string or DateTime object - * @param int|string|DateTime $end UNIX timestamp, strtotime() valid string or DateTime object - * @param string $fieldName Name of database field to compare with - * @param string|DateTimeZone $timezone Timezone string or DateTimeZone object - * @return string Partial SQL string. - * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/time.html#TimeHelper::daysAsSql - */ - public static function daysAsSql($begin, $end, $fieldName, $timezone = null) { - $begin = static::fromString($begin, $timezone); - $end = static::fromString($end, $timezone); - $begin = date('Y-m-d', $begin) . ' 00:00:00'; - $end = date('Y-m-d', $end) . ' 23:59:59'; - - return "($fieldName >= '$begin') AND ($fieldName <= '$end')"; - } - -/** - * Returns a partial SQL string to search for all records between two times - * occurring on the same day. - * - * @param int|string|DateTime $dateString UNIX timestamp, strtotime() valid string or DateTime object - * @param string $fieldName Name of database field to compare with - * @param string|DateTimeZone $timezone Timezone string or DateTimeZone object - * @return string Partial SQL string. - * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/time.html#TimeHelper::dayAsSql - */ - public static function dayAsSql($dateString, $fieldName, $timezone = null) { - return static::daysAsSql($dateString, $dateString, $fieldName, $timezone); - } - -/** - * Returns true if given datetime string is today. - * - * @param int|string|DateTime $dateString UNIX timestamp, strtotime() valid string or DateTime object - * @param string|DateTimeZone $timezone Timezone string or DateTimeZone object - * @return bool True if datetime string is today - * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/time.html#TimeHelper::isToday - */ - public static function isToday($dateString, $timezone = null) { - $timestamp = static::fromString($dateString, $timezone); - $now = static::fromString('now', $timezone); - return date('Y-m-d', $timestamp) === date('Y-m-d', $now); - } - -/** - * Returns true if given datetime string is in the future. - * - * @param int|string|DateTime $dateString UNIX timestamp, strtotime() valid string or DateTime object - * @param string|DateTimeZone $timezone Timezone string or DateTimeZone object - * @return bool True if datetime string is in the future - * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/time.html#TimeHelper::isFuture - */ - public static function isFuture($dateString, $timezone = null) { - $timestamp = static::fromString($dateString, $timezone); - return $timestamp > time(); - } - -/** - * Returns true if given datetime string is in the past. - * - * @param int|string|DateTime $dateString UNIX timestamp, strtotime() valid string or DateTime object - * @param string|DateTimeZone $timezone Timezone string or DateTimeZone object - * @return bool True if datetime string is in the past - * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/time.html#TimeHelper::isPast - */ - public static function isPast($dateString, $timezone = null) { - $timestamp = static::fromString($dateString, $timezone); - return $timestamp < time(); - } - -/** - * Returns true if given datetime string is within this week. - * - * @param int|string|DateTime $dateString UNIX timestamp, strtotime() valid string or DateTime object - * @param string|DateTimeZone $timezone Timezone string or DateTimeZone object - * @return bool True if datetime string is within current week - * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/time.html#TimeHelper::isThisWeek - */ - public static function isThisWeek($dateString, $timezone = null) { - $timestamp = static::fromString($dateString, $timezone); - $now = static::fromString('now', $timezone); - return date('W o', $timestamp) === date('W o', $now); - } - -/** - * Returns true if given datetime string is within this month - * - * @param int|string|DateTime $dateString UNIX timestamp, strtotime() valid string or DateTime object - * @param string|DateTimeZone $timezone Timezone string or DateTimeZone object - * @return bool True if datetime string is within current month - * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/time.html#TimeHelper::isThisMonth - */ - public static function isThisMonth($dateString, $timezone = null) { - $timestamp = static::fromString($dateString, $timezone); - $now = static::fromString('now', $timezone); - return date('m Y', $timestamp) === date('m Y', $now); - } - -/** - * Returns true if given datetime string is within current year. - * - * @param int|string|DateTime $dateString UNIX timestamp, strtotime() valid string or DateTime object - * @param string|DateTimeZone $timezone Timezone string or DateTimeZone object - * @return bool True if datetime string is within current year - * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/time.html#TimeHelper::isThisYear - */ - public static function isThisYear($dateString, $timezone = null) { - $timestamp = static::fromString($dateString, $timezone); - $now = static::fromString('now', $timezone); - return date('Y', $timestamp) === date('Y', $now); - } - -/** - * Returns true if given datetime string was yesterday. - * - * @param int|string|DateTime $dateString UNIX timestamp, strtotime() valid string or DateTime object - * @param string|DateTimeZone $timezone Timezone string or DateTimeZone object - * @return bool True if datetime string was yesterday - * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/time.html#TimeHelper::wasYesterday - */ - public static function wasYesterday($dateString, $timezone = null) { - $timestamp = static::fromString($dateString, $timezone); - $yesterday = static::fromString('yesterday', $timezone); - return date('Y-m-d', $timestamp) === date('Y-m-d', $yesterday); - } - -/** - * Returns true if given datetime string is tomorrow. - * - * @param int|string|DateTime $dateString UNIX timestamp, strtotime() valid string or DateTime object - * @param string|DateTimeZone $timezone Timezone string or DateTimeZone object - * @return bool True if datetime string was yesterday - * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/time.html#TimeHelper::isTomorrow - */ - public static function isTomorrow($dateString, $timezone = null) { - $timestamp = static::fromString($dateString, $timezone); - $tomorrow = static::fromString('tomorrow', $timezone); - return date('Y-m-d', $timestamp) === date('Y-m-d', $tomorrow); - } - -/** - * Returns the quarter - * - * @param int|string|DateTime $dateString UNIX timestamp, strtotime() valid string or DateTime object - * @param bool $range if true returns a range in Y-m-d format - * @return int|array 1, 2, 3, or 4 quarter of year or array if $range true - * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/time.html#TimeHelper::toQuarter - */ - public static function toQuarter($dateString, $range = false) { - $time = static::fromString($dateString); - $date = (int)ceil(date('m', $time) / 3); - if ($range === false) { - return $date; - } - - $year = date('Y', $time); - switch ($date) { - case 1: - return array($year . '-01-01', $year . '-03-31'); - case 2: - return array($year . '-04-01', $year . '-06-30'); - case 3: - return array($year . '-07-01', $year . '-09-30'); - case 4: - return array($year . '-10-01', $year . '-12-31'); - } - } - -/** - * Returns a UNIX timestamp from a textual datetime description. Wrapper for PHP function strtotime(). - * - * @param int|string|DateTime $dateString UNIX timestamp, strtotime() valid string or DateTime object - * @param string|DateTimeZone $timezone Timezone string or DateTimeZone object - * @return int Unix timestamp - * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/time.html#TimeHelper::toUnix - */ - public static function toUnix($dateString, $timezone = null) { - return static::fromString($dateString, $timezone); - } - -/** - * Returns a formatted date in server's timezone. - * - * If a DateTime object is given or the dateString has a timezone - * segment, the timezone parameter will be ignored. - * - * If no timezone parameter is given and no DateTime object, the passed $dateString will be - * considered to be in the UTC timezone. - * - * @param int|string|DateTime $dateString UNIX timestamp, strtotime() valid string or DateTime object - * @param string|DateTimeZone $timezone Timezone string or DateTimeZone object - * @param string $format date format string - * @return mixed Formatted date - * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/time.html#TimeHelper::toServer - */ - public static function toServer($dateString, $timezone = null, $format = 'Y-m-d H:i:s') { - if ($timezone === null) { - $timezone = new DateTimeZone('UTC'); - } elseif (is_string($timezone)) { - $timezone = new DateTimeZone($timezone); - } elseif (!($timezone instanceof DateTimeZone)) { - return false; - } - - if ($dateString instanceof DateTime) { - $date = $dateString; - } elseif (is_int($dateString) || is_numeric($dateString)) { - $dateString = (int)$dateString; - - $date = new DateTime('@' . $dateString); - $date->setTimezone($timezone); - } else { - $date = new DateTime($dateString, $timezone); - } - - $date->setTimezone(new DateTimeZone(date_default_timezone_get())); - return $date->format($format); - } - -/** - * Returns a date formatted for Atom RSS feeds. - * - * @param string $dateString Datetime string or Unix timestamp - * @param string|DateTimeZone $timezone Timezone string or DateTimeZone object - * @return string Formatted date string - * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/time.html#TimeHelper::toAtom - */ - public static function toAtom($dateString, $timezone = null) { - return date('Y-m-d\TH:i:s\Z', static::fromString($dateString, $timezone)); - } - -/** - * Formats date for RSS feeds - * - * @param int|string|DateTime $dateString UNIX timestamp, strtotime() valid string or DateTime object - * @param string|DateTimeZone $timezone Timezone string or DateTimeZone object - * @return string Formatted date string - * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/time.html#TimeHelper::toRSS - */ - public static function toRSS($dateString, $timezone = null) { - $date = static::fromString($dateString, $timezone); - - if ($timezone === null) { - return date("r", $date); - } - - $userOffset = $timezone; - if (!is_numeric($timezone)) { - if (!is_object($timezone)) { - $timezone = new DateTimeZone($timezone); - } - $currentDate = new DateTime('@' . $date); - $currentDate->setTimezone($timezone); - $userOffset = $timezone->getOffset($currentDate) / 60 / 60; - } - - $timezone = '+0000'; - if ($userOffset != 0) { - $hours = (int)floor(abs($userOffset)); - $minutes = (int)(fmod(abs($userOffset), $hours) * 60); - $timezone = ($userOffset < 0 ? '-' : '+') . str_pad($hours, 2, '0', STR_PAD_LEFT) . str_pad($minutes, 2, '0', STR_PAD_LEFT); - } - return date('D, d M Y H:i:s', $date) . ' ' . $timezone; - } - -/** - * Returns either a relative or a formatted absolute date depending - * on the difference between the current time and given datetime. - * $datetime should be in a *strtotime* - parsable format, like MySQL's datetime datatype. - * - * ### Options: - * - * - `format` => a fall back format if the relative time is longer than the duration specified by end - * - `accuracy` => Specifies how accurate the date should be described (array) - * - year => The format if years > 0 (default "day") - * - month => The format if months > 0 (default "day") - * - week => The format if weeks > 0 (default "day") - * - day => The format if weeks > 0 (default "hour") - * - hour => The format if hours > 0 (default "minute") - * - minute => The format if minutes > 0 (default "minute") - * - second => The format if seconds > 0 (default "second") - * - `end` => The end of relative time telling - * - `relativeString` => The printf compatible string when outputting past relative time - * - `relativeStringFuture` => The printf compatible string when outputting future relative time - * - `absoluteString` => The printf compatible string when outputting absolute time - * - `userOffset` => Users offset from GMT (in hours) *Deprecated* use timezone instead. - * - `timezone` => The user timezone the timestamp should be formatted in. - * - * Relative dates look something like this: - * - * - 3 weeks, 4 days ago - * - 15 seconds ago - * - * Default date formatting is d/m/yy e.g: on 18/2/09 - * - * The returned string includes 'ago' or 'on' and assumes you'll properly add a word - * like 'Posted ' before the function output. - * - * NOTE: If the difference is one week or more, the lowest level of accuracy is day - * - * @param int|string|DateTime $dateTime Datetime UNIX timestamp, strtotime() valid string or DateTime object - * @param array $options Default format if timestamp is used in $dateString - * @return string Relative time string. - * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/time.html#TimeHelper::timeAgoInWords - */ - public static function timeAgoInWords($dateTime, $options = array()) { - $timezone = null; - $accuracies = static::$wordAccuracy; - $format = static::$wordFormat; - $relativeEnd = static::$wordEnd; - $relativeStringPast = __d('cake', '%s ago'); - $relativeStringFuture = __d('cake', 'in %s'); - $absoluteString = __d('cake', 'on %s'); - - if (is_string($options)) { - $format = $options; - } elseif (!empty($options)) { - if (isset($options['timezone'])) { - $timezone = $options['timezone']; - } elseif (isset($options['userOffset'])) { - $timezone = $options['userOffset']; - } - - if (isset($options['accuracy'])) { - if (is_array($options['accuracy'])) { - $accuracies = array_merge($accuracies, $options['accuracy']); - } else { - foreach ($accuracies as $key => $level) { - $accuracies[$key] = $options['accuracy']; - } - } - } - - if (isset($options['format'])) { - $format = $options['format']; - } - if (isset($options['end'])) { - $relativeEnd = $options['end']; - } - if (isset($options['relativeString'])) { - $relativeStringPast = $options['relativeString']; - unset($options['relativeString']); - } - if (isset($options['relativeStringFuture'])) { - $relativeStringFuture = $options['relativeStringFuture']; - unset($options['relativeStringFuture']); - } - if (isset($options['absoluteString'])) { - $absoluteString = $options['absoluteString']; - unset($options['absoluteString']); - } - unset($options['end'], $options['format']); - } - - $now = static::fromString(time(), $timezone); - $inSeconds = static::fromString($dateTime, $timezone); - $isFuture = ($inSeconds > $now); - - if ($isFuture) { - $startTime = $now; - $endTime = $inSeconds; - } else { - $startTime = $inSeconds; - $endTime = $now; - } - $diff = $endTime - $startTime; - - if ($diff === 0) { - return __d('cake', 'just now', 'just now'); - } - - $isAbsoluteDate = $diff > abs($now - static::fromString($relativeEnd)); - if ($isAbsoluteDate) { - if (strpos($format, '%') === false) { - $date = date($format, $inSeconds); - } else { - $date = static::_strftime($format, $inSeconds); - } - return sprintf($absoluteString, $date); - } - - $years = $months = $weeks = $days = $hours = $minutes = $seconds = 0; - - // If more than a week, then take into account the length of months - if ($diff >= 604800) { - list($future['H'], $future['i'], $future['s'], $future['d'], $future['m'], $future['Y']) = explode('/', date('H/i/s/d/m/Y', $endTime)); - list($past['H'], $past['i'], $past['s'], $past['d'], $past['m'], $past['Y']) = explode('/', date('H/i/s/d/m/Y', $startTime)); - - $years = $future['Y'] - $past['Y']; - $months = $future['m'] + ((12 * $years) - $past['m']); - - if ($months >= 12) { - $years = floor($months / 12); - $months = $months - ($years * 12); - } - if ($future['m'] < $past['m'] && $future['Y'] - $past['Y'] === 1) { - $years--; - } - - if ($future['d'] >= $past['d']) { - $days = $future['d'] - $past['d']; - } else { - $daysInPastMonth = date('t', $startTime); - $daysInFutureMonth = date('t', mktime(0, 0, 0, $future['m'] - 1, 1, $future['Y'])); - - if ($isFuture) { - $days = ($daysInFutureMonth - $past['d']) + $future['d']; - } else { - $days = ($daysInPastMonth - $past['d']) + $future['d']; - } - - if ($future['m'] != $past['m']) { - $months--; - } - } - - if (!$months && $years >= 1 && $diff < ($years * 31536000)) { - $months = 11; - $years--; - } - - if ($months >= 12) { - $years = $years + 1; - $months = $months - 12; - } - - if ($days >= 7) { - $weeks = floor($days / 7); - $days = $days - ($weeks * 7); - } - } else { - $days = floor($diff / 86400); - $diff = $diff - ($days * 86400); - - $hours = floor($diff / 3600); - $diff = $diff - ($hours * 3600); - - $minutes = floor($diff / 60); - $diff = $diff - ($minutes * 60); - - $seconds = $diff; - } - - $accuracy = $accuracies['second']; - if ($years > 0) { - $accuracy = $accuracies['year']; - } elseif (abs($months) > 0) { - $accuracy = $accuracies['month']; - } elseif (abs($weeks) > 0) { - $accuracy = $accuracies['week']; - } elseif (abs($days) > 0) { - $accuracy = $accuracies['day']; - } elseif (abs($hours) > 0) { - $accuracy = $accuracies['hour']; - } elseif (abs($minutes) > 0) { - $accuracy = $accuracies['minute']; - } - - $accuracyNum = str_replace(array('year', 'month', 'week', 'day', 'hour', 'minute', 'second'), array(1, 2, 3, 4, 5, 6, 7), $accuracy); - - $relativeDate = array(); - if ($accuracyNum >= 1 && $years > 0) { - $relativeDate[] = __dn('cake', '%d year', '%d years', $years, $years); - } - if ($accuracyNum >= 2 && $months > 0) { - $relativeDate[] = __dn('cake', '%d month', '%d months', $months, $months); - } - if ($accuracyNum >= 3 && $weeks > 0) { - $relativeDate[] = __dn('cake', '%d week', '%d weeks', $weeks, $weeks); - } - if ($accuracyNum >= 4 && $days > 0) { - $relativeDate[] = __dn('cake', '%d day', '%d days', $days, $days); - } - if ($accuracyNum >= 5 && $hours > 0) { - $relativeDate[] = __dn('cake', '%d hour', '%d hours', $hours, $hours); - } - if ($accuracyNum >= 6 && $minutes > 0) { - $relativeDate[] = __dn('cake', '%d minute', '%d minutes', $minutes, $minutes); - } - if ($accuracyNum >= 7 && $seconds > 0) { - $relativeDate[] = __dn('cake', '%d second', '%d seconds', $seconds, $seconds); - } - $relativeDate = implode(', ', $relativeDate); - - if ($relativeDate) { - $relativeString = ($isFuture) ? $relativeStringFuture : $relativeStringPast; - return sprintf($relativeString, $relativeDate); - } - - if ($isFuture) { - $strings = array( - 'second' => __d('cake', 'in about a second'), - 'minute' => __d('cake', 'in about a minute'), - 'hour' => __d('cake', 'in about an hour'), - 'day' => __d('cake', 'in about a day'), - 'week' => __d('cake', 'in about a week'), - 'year' => __d('cake', 'in about a year') - ); - } else { - $strings = array( - 'second' => __d('cake', 'about a second ago'), - 'minute' => __d('cake', 'about a minute ago'), - 'hour' => __d('cake', 'about an hour ago'), - 'day' => __d('cake', 'about a day ago'), - 'week' => __d('cake', 'about a week ago'), - 'year' => __d('cake', 'about a year ago') - ); - } - - return $strings[$accuracy]; - } - -/** - * Returns true if specified datetime was within the interval specified, else false. - * - * @param string|int $timeInterval the numeric value with space then time type. - * Example of valid types: 6 hours, 2 days, 1 minute. - * @param int|string|DateTime $dateString UNIX timestamp, strtotime() valid string or DateTime object - * @param string|DateTimeZone $timezone Timezone string or DateTimeZone object - * @return bool - * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/time.html#TimeHelper::wasWithinLast - */ - public static function wasWithinLast($timeInterval, $dateString, $timezone = null) { - $tmp = str_replace(' ', '', $timeInterval); - if (is_numeric($tmp)) { - $timeInterval = $tmp . ' ' . __d('cake', 'days'); - } - - $date = static::fromString($dateString, $timezone); - $interval = static::fromString('-' . $timeInterval); - $now = static::fromString('now', $timezone); - - return $date >= $interval && $date <= $now; - } - -/** - * Returns true if specified datetime is within the interval specified, else false. - * - * @param string|int $timeInterval the numeric value with space then time type. - * Example of valid types: 6 hours, 2 days, 1 minute. - * @param int|string|DateTime $dateString UNIX timestamp, strtotime() valid string or DateTime object - * @param string|DateTimeZone $timezone Timezone string or DateTimeZone object - * @return bool - */ - public static function isWithinNext($timeInterval, $dateString, $timezone = null) { - $tmp = str_replace(' ', '', $timeInterval); - if (is_numeric($tmp)) { - $timeInterval = $tmp . ' ' . __d('cake', 'days'); - } - - $date = static::fromString($dateString, $timezone); - $interval = static::fromString('+' . $timeInterval); - $now = static::fromString('now', $timezone); - - return $date <= $interval && $date >= $now; - } - -/** - * Returns gmt as a UNIX timestamp. - * - * @param int|string|DateTime $dateString UNIX timestamp, strtotime() valid string or DateTime object - * @return int UNIX timestamp - * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/time.html#TimeHelper::gmt - */ - public static function gmt($dateString = null) { - $time = time(); - if ($dateString) { - $time = static::fromString($dateString); - } - return gmmktime( - (int)date('G', $time), - (int)date('i', $time), - (int)date('s', $time), - (int)date('n', $time), - (int)date('j', $time), - (int)date('Y', $time) - ); - } - -/** - * Returns a formatted date string, given either a UNIX timestamp or a valid strtotime() date string. - * This function also accepts a time string and a format string as first and second parameters. - * In that case this function behaves as a wrapper for TimeHelper::i18nFormat() - * - * ## Examples - * - * Create localized & formatted time: - * - * ``` - * CakeTime::format('2012-02-15', '%m-%d-%Y'); // returns 02-15-2012 - * CakeTime::format('2012-02-15 23:01:01', '%c'); // returns preferred date and time based on configured locale - * CakeTime::format('0000-00-00', '%d-%m-%Y', 'N/A'); // return N/A becuase an invalid date was passed - * CakeTime::format('2012-02-15 23:01:01', '%c', 'N/A', 'America/New_York'); // converts passed date to timezone - * ``` - * - * @param int|string|DateTime $date UNIX timestamp, strtotime() valid string or DateTime object (or a date format string) - * @param int|string|DateTime $format date format string (or UNIX timestamp, strtotime() valid string or DateTime object) - * @param bool|string $default if an invalid date is passed it will output supplied default value. Pass false if you want raw conversion value - * @param string|DateTimeZone $timezone Timezone string or DateTimeZone object - * @return string Formatted date string - * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/time.html#TimeHelper::format - * @see CakeTime::i18nFormat() - */ - public static function format($date, $format = null, $default = false, $timezone = null) { - //Backwards compatible params re-order test - $time = static::fromString($format, $timezone); - - if ($time === false) { - return static::i18nFormat($date, $format, $default, $timezone); - } - return date($date, $time); - } - -/** - * Returns a formatted date string, given either a UNIX timestamp or a valid strtotime() date string. - * It takes into account the default date format for the current language if a LC_TIME file is used. - * - * @param int|string|DateTime $date UNIX timestamp, strtotime() valid string or DateTime object - * @param string $format strftime format string. - * @param bool|string $default if an invalid date is passed it will output supplied default value. Pass false if you want raw conversion value - * @param string|DateTimeZone $timezone Timezone string or DateTimeZone object - * @return string Formatted and translated date string - * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/time.html#TimeHelper::i18nFormat - */ - public static function i18nFormat($date, $format = null, $default = false, $timezone = null) { - $timestamp = static::fromString($date, $timezone); - if ($timestamp === false && $default !== false) { - return $default; - } - if ($timestamp === false) { - return ''; - } - if (empty($format)) { - $format = '%x'; - } - $convertedFormat = static::convertSpecifiers($format, $timestamp); - return static::_strftimeWithTimezone($convertedFormat, $timestamp, $date, $timezone); - } - -/** - * Get list of timezone identifiers - * - * @param int|string $filter A regex to filter identifier - * Or one of DateTimeZone class constants (PHP 5.3 and above) - * @param string $country A two-letter ISO 3166-1 compatible country code. - * This option is only used when $filter is set to DateTimeZone::PER_COUNTRY (available only in PHP 5.3 and above) - * @param bool|array $options If true (default value) groups the identifiers list by primary region. - * Otherwise, an array containing `group`, `abbr`, `before`, and `after` keys. - * Setting `group` and `abbr` to true will group results and append timezone - * abbreviation in the display value. Set `before` and `after` to customize - * the abbreviation wrapper. - * @return array List of timezone identifiers - * @since 2.2 - * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/time.html#TimeHelper::listTimezones - */ - public static function listTimezones($filter = null, $country = null, $options = array()) { - if (is_bool($options)) { - $options = array( - 'group' => $options, - ); - } - $defaults = array( - 'group' => true, - 'abbr' => false, - 'before' => ' - ', - 'after' => null, - ); - $options += $defaults; - $group = $options['group']; - - $regex = null; - if (is_string($filter)) { - $regex = $filter; - $filter = null; - } - if (version_compare(PHP_VERSION, '5.3.0', '<')) { - if ($regex === null) { - $regex = '#^((Africa|America|Antartica|Arctic|Asia|Atlantic|Australia|Europe|Indian|Pacific)/|UTC)#'; - } - $identifiers = DateTimeZone::listIdentifiers(); - } else { - if ($filter === null) { - $filter = DateTimeZone::ALL; - } - $identifiers = DateTimeZone::listIdentifiers($filter, $country); - } - - if ($regex) { - foreach ($identifiers as $key => $tz) { - if (!preg_match($regex, $tz)) { - unset($identifiers[$key]); - } - } - } - - if ($group) { - $return = array(); - $now = time(); - $before = $options['before']; - $after = $options['after']; - foreach ($identifiers as $key => $tz) { - $abbr = null; - if ($options['abbr']) { - $dateTimeZone = new DateTimeZone($tz); - $trans = $dateTimeZone->getTransitions($now, $now); - $abbr = isset($trans[0]['abbr']) ? - $before . $trans[0]['abbr'] . $after : - null; - } - $item = explode('/', $tz, 2); - if (isset($item[1])) { - $return[$item[0]][$tz] = $item[1] . $abbr; - } else { - $return[$item[0]] = array($tz => $item[0] . $abbr); - } - } - return $return; - } - return array_combine($identifiers, $identifiers); - } - -/** - * Multibyte wrapper for strftime. - * - * Handles utf8_encoding the result of strftime when necessary. - * - * @param string $format Format string. - * @param int $timestamp Timestamp to format. - * @return string formatted string with correct encoding. - */ - protected static function _strftime($format, $timestamp) { - $format = strftime($format, $timestamp); - $encoding = Configure::read('App.encoding'); - if (!empty($encoding) && $encoding === 'UTF-8') { - if (function_exists('mb_check_encoding')) { - $valid = mb_check_encoding($format, $encoding); - } else { - $valid = Multibyte::checkMultibyte($format); - } - if (!$valid) { - $format = utf8_encode($format); - } - } - return $format; - } - -/** - * Multibyte wrapper for strftime. - * - * Adjusts the timezone when necessary before formatting the time. - * - * @param string $format Format string. - * @param int $timestamp Timestamp to format. - * @param int|string|DateTime $date Timestamp, strtotime() valid string or DateTime object. - * @param string|DateTimeZone $timezone Timezone string or DateTimeZone object. - * @return string Formatted date string with correct encoding. - */ - protected static function _strftimeWithTimezone($format, $timestamp, $date, $timezone) { - $serverTimeZone = date_default_timezone_get(); - if ( - !empty($timezone) && - $date instanceof DateTime && - $date->getTimezone()->getName() != $serverTimeZone - ) { - date_default_timezone_set($timezone); - } - $result = static::_strftime($format, $timestamp); - date_default_timezone_set($serverTimeZone); - return $result; - } +class CakeTime +{ + + /** + * The format to use when formatting a time using `CakeTime::nice()` + * + * The format should use the locale strings as defined in the PHP docs under + * `strftime` (http://php.net/manual/en/function.strftime.php) + * + * @var string + * @see CakeTime::format() + */ + public static $niceFormat = '%a, %b %eS %Y, %H:%M'; + + /** + * The format to use when formatting a time using `CakeTime::timeAgoInWords()` + * and the difference is more than `CakeTime::$wordEnd` + * + * @var string + * @see CakeTime::timeAgoInWords() + */ + public static $wordFormat = 'j/n/y'; + + /** + * The format to use when formatting a time using `CakeTime::niceShort()` + * and the difference is between 3 and 7 days + * + * @var string + * @see CakeTime::niceShort() + */ + public static $niceShortFormat = '%B %d, %H:%M'; + + /** + * The format to use when formatting a time using `CakeTime::timeAgoInWords()` + * and the difference is less than `CakeTime::$wordEnd` + * + * @var array + * @see CakeTime::timeAgoInWords() + */ + public static $wordAccuracy = [ + 'year' => 'day', + 'month' => 'day', + 'week' => 'day', + 'day' => 'hour', + 'hour' => 'minute', + 'minute' => 'minute', + 'second' => 'second', + ]; + + /** + * The end of relative time telling + * + * @var string + * @see CakeTime::timeAgoInWords() + */ + public static $wordEnd = '+1 month'; + + /** + * Temporary variable containing the timestamp value, used internally in convertSpecifiers() + * + * @var int + */ + protected static $_time = null; + + /** + * Returns server's offset from GMT in seconds. + * + * @return int Offset + * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/time.html#TimeHelper::serverOffset + */ + public static function serverOffset() + { + return date('Z', time()); + } + + /** + * Returns a nicely formatted date string for given Datetime string. + * + * See http://php.net/manual/en/function.strftime.php for information on formatting + * using locale strings. + * + * @param int|string|DateTime $date UNIX timestamp, strtotime() valid string or DateTime object + * @param string|DateTimeZone $timezone Timezone string or DateTimeZone object + * @param string $format The format to use. If null, `CakeTime::$niceFormat` is used + * @return string Formatted date string + * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/time.html#TimeHelper::nice + */ + public static function nice($date = null, $timezone = null, $format = null) + { + if (!$date) { + $date = time(); + } + $timestamp = static::fromString($date, $timezone); + if (!$format) { + $format = static::$niceFormat; + } + $convertedFormat = static::convertSpecifiers($format, $timestamp); + return static::_strftimeWithTimezone($convertedFormat, $timestamp, $date, $timezone); + } + + /** + * Returns a timestamp, given either a UNIX timestamp or a valid strtotime() date string. + * + * @param int|string|DateTime $dateString UNIX timestamp, strtotime() valid string or DateTime object + * @param string|DateTimeZone $timezone Timezone string or DateTimeZone object + * @return int|false Parsed given timezone timestamp. + * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/time.html#TimeHelper::fromString + */ + public static function fromString($dateString, $timezone = null) + { + if (empty($dateString)) { + return false; + } + + $containsDummyDate = (is_string($dateString) && substr($dateString, 0, 10) === '0000-00-00'); + if ($containsDummyDate) { + return false; + } + + if (is_int($dateString) || is_numeric($dateString)) { + $date = (int)$dateString; + } else if ($dateString instanceof DateTime && + $dateString->getTimezone()->getName() != date_default_timezone_get() + ) { + $clone = clone $dateString; + $clone->setTimezone(new DateTimeZone(date_default_timezone_get())); + $date = (int)$clone->format('U') + $clone->getOffset(); + } else if ($dateString instanceof DateTime) { + $date = (int)$dateString->format('U'); + } else { + $date = strtotime($dateString); + } + + if ($date === -1 || empty($date)) { + return false; + } + + if ($timezone === null) { + $timezone = Configure::read('Config.timezone'); + } + + if ($timezone !== null) { + return static::convert($date, $timezone); + } + return $date; + } + + /** + * Converts given time (in server's time zone) to user's local time, given his/her timezone. + * + * @param int $serverTime Server's timestamp. + * @param string|DateTimeZone $timezone User's timezone string or DateTimeZone object. + * @return int User's timezone timestamp. + * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/time.html#TimeHelper::convert + */ + public static function convert($serverTime, $timezone) + { + static $serverTimezone = null; + if ($serverTimezone === null || (date_default_timezone_get() !== $serverTimezone->getName())) { + $serverTimezone = new DateTimeZone(date_default_timezone_get()); + } + $serverOffset = $serverTimezone->getOffset(new DateTime('@' . $serverTime)); + $gmtTime = $serverTime - $serverOffset; + if (is_numeric($timezone)) { + $userOffset = $timezone * (60 * 60); + } else { + $timezone = static::timezone($timezone); + $userOffset = $timezone->getOffset(new DateTime('@' . $gmtTime)); + } + $userTime = $gmtTime + $userOffset; + return (int)$userTime; + } + + /** + * Returns a timezone object from a string or the user's timezone object + * + * @param string|DateTimeZone $timezone Timezone string or DateTimeZone object + * If null it tries to get timezone from 'Config.timezone' config var + * @return DateTimeZone Timezone object + * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/time.html#TimeHelper::timezone + */ + public static function timezone($timezone = null) + { + static $tz = null; + + if (is_object($timezone)) { + if ($tz === null || $tz->getName() !== $timezone->getName()) { + $tz = $timezone; + } + } else { + if ($timezone === null) { + $timezone = Configure::read('Config.timezone'); + if ($timezone === null) { + $timezone = date_default_timezone_get(); + } + } + + if ($tz === null || $tz->getName() !== $timezone) { + $tz = new DateTimeZone($timezone); + } + } + + return $tz; + } + + /** + * Multibyte wrapper for strftime. + * + * Adjusts the timezone when necessary before formatting the time. + * + * @param string $format Format string. + * @param int $timestamp Timestamp to format. + * @param int|string|DateTime $date Timestamp, strtotime() valid string or DateTime object. + * @param string|DateTimeZone $timezone Timezone string or DateTimeZone object. + * @return string Formatted date string with correct encoding. + */ + protected static function _strftimeWithTimezone($format, $timestamp, $date, $timezone) + { + $serverTimeZone = date_default_timezone_get(); + if ( + !empty($timezone) && + $date instanceof DateTime && + $date->getTimezone()->getName() != $serverTimeZone + ) { + date_default_timezone_set($timezone); + } + $result = static::_strftime($format, $timestamp); + date_default_timezone_set($serverTimeZone); + return $result; + } + + /** + * Multibyte wrapper for strftime. + * + * Handles utf8_encoding the result of strftime when necessary. + * + * @param string $format Format string. + * @param int $timestamp Timestamp to format. + * @return string formatted string with correct encoding. + */ + protected static function _strftime($format, $timestamp) + { + $format = strftime($format, $timestamp); + $encoding = Configure::read('App.encoding'); + if (!empty($encoding) && $encoding === 'UTF-8') { + if (function_exists('mb_check_encoding')) { + $valid = mb_check_encoding($format, $encoding); + } else { + $valid = Multibyte::checkMultibyte($format); + } + if (!$valid) { + $format = utf8_encode($format); + } + } + return $format; + } + + /** + * Returns a formatted descriptive date string for given datetime string. + * + * If the given date is today, the returned string could be "Today, 16:54". + * If the given date is tomorrow, the returned string could be "Tomorrow, 16:54". + * If the given date was yesterday, the returned string could be "Yesterday, 16:54". + * If the given date is within next or last week, the returned string could be "On Thursday, 16:54". + * If $dateString's year is the current year, the returned string does not + * include mention of the year. + * + * @param int|string|DateTime $date UNIX timestamp, strtotime() valid string or DateTime object + * @param string|DateTimeZone $timezone Timezone string or DateTimeZone object + * @return string Described, relative date string + * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/time.html#TimeHelper::niceShort + */ + public static function niceShort($date = null, $timezone = null) + { + if (!$date) { + $date = time(); + } + $timestamp = static::fromString($date, $timezone); + + if (static::isToday($date, $timezone)) { + $formattedDate = static::_strftimeWithTimezone("%H:%M", $timestamp, $date, $timezone); + return __d('cake', 'Today, %s', $formattedDate); + } + if (static::wasYesterday($date, $timezone)) { + $formattedDate = static::_strftimeWithTimezone("%H:%M", $timestamp, $date, $timezone); + return __d('cake', 'Yesterday, %s', $formattedDate); + } + if (static::isTomorrow($date, $timezone)) { + $formattedDate = static::_strftimeWithTimezone("%H:%M", $timestamp, $date, $timezone); + return __d('cake', 'Tomorrow, %s', $formattedDate); + } + + $d = static::_strftimeWithTimezone("%w", $timestamp, $date, $timezone); + $day = [ + __d('cake', 'Sunday'), + __d('cake', 'Monday'), + __d('cake', 'Tuesday'), + __d('cake', 'Wednesday'), + __d('cake', 'Thursday'), + __d('cake', 'Friday'), + __d('cake', 'Saturday') + ]; + if (static::wasWithinLast('7 days', $date, $timezone)) { + $formattedDate = static::_strftimeWithTimezone(static::$niceShortFormat, $timestamp, $date, $timezone); + return sprintf('%s %s', $day[$d], $formattedDate); + } + if (static::isWithinNext('7 days', $date, $timezone)) { + $formattedDate = static::_strftimeWithTimezone(static::$niceShortFormat, $timestamp, $date, $timezone); + return __d('cake', 'On %s %s', $day[$d], $formattedDate); + } + + $y = ''; + if (!static::isThisYear($timestamp)) { + $y = ' %Y'; + } + $format = static::convertSpecifiers("%b %eS{$y}, %H:%M", $timestamp); + return static::_strftimeWithTimezone($format, $timestamp, $date, $timezone); + } + + /** + * Returns true if given datetime string is today. + * + * @param int|string|DateTime $dateString UNIX timestamp, strtotime() valid string or DateTime object + * @param string|DateTimeZone $timezone Timezone string or DateTimeZone object + * @return bool True if datetime string is today + * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/time.html#TimeHelper::isToday + */ + public static function isToday($dateString, $timezone = null) + { + $timestamp = static::fromString($dateString, $timezone); + $now = static::fromString('now', $timezone); + return date('Y-m-d', $timestamp) === date('Y-m-d', $now); + } + + /** + * Returns true if given datetime string was yesterday. + * + * @param int|string|DateTime $dateString UNIX timestamp, strtotime() valid string or DateTime object + * @param string|DateTimeZone $timezone Timezone string or DateTimeZone object + * @return bool True if datetime string was yesterday + * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/time.html#TimeHelper::wasYesterday + */ + public static function wasYesterday($dateString, $timezone = null) + { + $timestamp = static::fromString($dateString, $timezone); + $yesterday = static::fromString('yesterday', $timezone); + return date('Y-m-d', $timestamp) === date('Y-m-d', $yesterday); + } + + /** + * Returns true if given datetime string is tomorrow. + * + * @param int|string|DateTime $dateString UNIX timestamp, strtotime() valid string or DateTime object + * @param string|DateTimeZone $timezone Timezone string or DateTimeZone object + * @return bool True if datetime string was yesterday + * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/time.html#TimeHelper::isTomorrow + */ + public static function isTomorrow($dateString, $timezone = null) + { + $timestamp = static::fromString($dateString, $timezone); + $tomorrow = static::fromString('tomorrow', $timezone); + return date('Y-m-d', $timestamp) === date('Y-m-d', $tomorrow); + } + + /** + * Returns true if specified datetime was within the interval specified, else false. + * + * @param string|int $timeInterval the numeric value with space then time type. + * Example of valid types: 6 hours, 2 days, 1 minute. + * @param int|string|DateTime $dateString UNIX timestamp, strtotime() valid string or DateTime object + * @param string|DateTimeZone $timezone Timezone string or DateTimeZone object + * @return bool + * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/time.html#TimeHelper::wasWithinLast + */ + public static function wasWithinLast($timeInterval, $dateString, $timezone = null) + { + $tmp = str_replace(' ', '', $timeInterval); + if (is_numeric($tmp)) { + $timeInterval = $tmp . ' ' . __d('cake', 'days'); + } + + $date = static::fromString($dateString, $timezone); + $interval = static::fromString('-' . $timeInterval); + $now = static::fromString('now', $timezone); + + return $date >= $interval && $date <= $now; + } + + /** + * Returns true if specified datetime is within the interval specified, else false. + * + * @param string|int $timeInterval the numeric value with space then time type. + * Example of valid types: 6 hours, 2 days, 1 minute. + * @param int|string|DateTime $dateString UNIX timestamp, strtotime() valid string or DateTime object + * @param string|DateTimeZone $timezone Timezone string or DateTimeZone object + * @return bool + */ + public static function isWithinNext($timeInterval, $dateString, $timezone = null) + { + $tmp = str_replace(' ', '', $timeInterval); + if (is_numeric($tmp)) { + $timeInterval = $tmp . ' ' . __d('cake', 'days'); + } + + $date = static::fromString($dateString, $timezone); + $interval = static::fromString('+' . $timeInterval); + $now = static::fromString('now', $timezone); + + return $date <= $interval && $date >= $now; + } + + /** + * Returns true if given datetime string is within current year. + * + * @param int|string|DateTime $dateString UNIX timestamp, strtotime() valid string or DateTime object + * @param string|DateTimeZone $timezone Timezone string or DateTimeZone object + * @return bool True if datetime string is within current year + * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/time.html#TimeHelper::isThisYear + */ + public static function isThisYear($dateString, $timezone = null) + { + $timestamp = static::fromString($dateString, $timezone); + $now = static::fromString('now', $timezone); + return date('Y', $timestamp) === date('Y', $now); + } + + /** + * Returns a partial SQL string to search for all records between two times + * occurring on the same day. + * + * @param int|string|DateTime $dateString UNIX timestamp, strtotime() valid string or DateTime object + * @param string $fieldName Name of database field to compare with + * @param string|DateTimeZone $timezone Timezone string or DateTimeZone object + * @return string Partial SQL string. + * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/time.html#TimeHelper::dayAsSql + */ + public static function dayAsSql($dateString, $fieldName, $timezone = null) + { + return static::daysAsSql($dateString, $dateString, $fieldName, $timezone); + } + + /** + * Returns a partial SQL string to search for all records between two dates. + * + * @param int|string|DateTime $begin UNIX timestamp, strtotime() valid string or DateTime object + * @param int|string|DateTime $end UNIX timestamp, strtotime() valid string or DateTime object + * @param string $fieldName Name of database field to compare with + * @param string|DateTimeZone $timezone Timezone string or DateTimeZone object + * @return string Partial SQL string. + * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/time.html#TimeHelper::daysAsSql + */ + public static function daysAsSql($begin, $end, $fieldName, $timezone = null) + { + $begin = static::fromString($begin, $timezone); + $end = static::fromString($end, $timezone); + $begin = date('Y-m-d', $begin) . ' 00:00:00'; + $end = date('Y-m-d', $end) . ' 23:59:59'; + + return "($fieldName >= '$begin') AND ($fieldName <= '$end')"; + } + + /** + * Returns true if given datetime string is in the future. + * + * @param int|string|DateTime $dateString UNIX timestamp, strtotime() valid string or DateTime object + * @param string|DateTimeZone $timezone Timezone string or DateTimeZone object + * @return bool True if datetime string is in the future + * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/time.html#TimeHelper::isFuture + */ + public static function isFuture($dateString, $timezone = null) + { + $timestamp = static::fromString($dateString, $timezone); + return $timestamp > time(); + } + + /** + * Returns true if given datetime string is in the past. + * + * @param int|string|DateTime $dateString UNIX timestamp, strtotime() valid string or DateTime object + * @param string|DateTimeZone $timezone Timezone string or DateTimeZone object + * @return bool True if datetime string is in the past + * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/time.html#TimeHelper::isPast + */ + public static function isPast($dateString, $timezone = null) + { + $timestamp = static::fromString($dateString, $timezone); + return $timestamp < time(); + } + + /** + * Returns true if given datetime string is within this week. + * + * @param int|string|DateTime $dateString UNIX timestamp, strtotime() valid string or DateTime object + * @param string|DateTimeZone $timezone Timezone string or DateTimeZone object + * @return bool True if datetime string is within current week + * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/time.html#TimeHelper::isThisWeek + */ + public static function isThisWeek($dateString, $timezone = null) + { + $timestamp = static::fromString($dateString, $timezone); + $now = static::fromString('now', $timezone); + return date('W o', $timestamp) === date('W o', $now); + } + + /** + * Returns true if given datetime string is within this month + * + * @param int|string|DateTime $dateString UNIX timestamp, strtotime() valid string or DateTime object + * @param string|DateTimeZone $timezone Timezone string or DateTimeZone object + * @return bool True if datetime string is within current month + * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/time.html#TimeHelper::isThisMonth + */ + public static function isThisMonth($dateString, $timezone = null) + { + $timestamp = static::fromString($dateString, $timezone); + $now = static::fromString('now', $timezone); + return date('m Y', $timestamp) === date('m Y', $now); + } + + /** + * Returns the quarter + * + * @param int|string|DateTime $dateString UNIX timestamp, strtotime() valid string or DateTime object + * @param bool $range if true returns a range in Y-m-d format + * @return int|array 1, 2, 3, or 4 quarter of year or array if $range true + * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/time.html#TimeHelper::toQuarter + */ + public static function toQuarter($dateString, $range = false) + { + $time = static::fromString($dateString); + $date = (int)ceil(date('m', $time) / 3); + if ($range === false) { + return $date; + } + + $year = date('Y', $time); + switch ($date) { + case 1: + return [$year . '-01-01', $year . '-03-31']; + case 2: + return [$year . '-04-01', $year . '-06-30']; + case 3: + return [$year . '-07-01', $year . '-09-30']; + case 4: + return [$year . '-10-01', $year . '-12-31']; + } + } + + /** + * Returns a UNIX timestamp from a textual datetime description. Wrapper for PHP function strtotime(). + * + * @param int|string|DateTime $dateString UNIX timestamp, strtotime() valid string or DateTime object + * @param string|DateTimeZone $timezone Timezone string or DateTimeZone object + * @return int Unix timestamp + * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/time.html#TimeHelper::toUnix + */ + public static function toUnix($dateString, $timezone = null) + { + return static::fromString($dateString, $timezone); + } + + /** + * Returns a formatted date in server's timezone. + * + * If a DateTime object is given or the dateString has a timezone + * segment, the timezone parameter will be ignored. + * + * If no timezone parameter is given and no DateTime object, the passed $dateString will be + * considered to be in the UTC timezone. + * + * @param int|string|DateTime $dateString UNIX timestamp, strtotime() valid string or DateTime object + * @param string|DateTimeZone $timezone Timezone string or DateTimeZone object + * @param string $format date format string + * @return mixed Formatted date + * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/time.html#TimeHelper::toServer + */ + public static function toServer($dateString, $timezone = null, $format = 'Y-m-d H:i:s') + { + if ($timezone === null) { + $timezone = new DateTimeZone('UTC'); + } else if (is_string($timezone)) { + $timezone = new DateTimeZone($timezone); + } else if (!($timezone instanceof DateTimeZone)) { + return false; + } + + if ($dateString instanceof DateTime) { + $date = $dateString; + } else if (is_int($dateString) || is_numeric($dateString)) { + $dateString = (int)$dateString; + + $date = new DateTime('@' . $dateString); + $date->setTimezone($timezone); + } else { + $date = new DateTime($dateString, $timezone); + } + + $date->setTimezone(new DateTimeZone(date_default_timezone_get())); + return $date->format($format); + } + + /** + * Returns a date formatted for Atom RSS feeds. + * + * @param string $dateString Datetime string or Unix timestamp + * @param string|DateTimeZone $timezone Timezone string or DateTimeZone object + * @return string Formatted date string + * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/time.html#TimeHelper::toAtom + */ + public static function toAtom($dateString, $timezone = null) + { + return date('Y-m-d\TH:i:s\Z', static::fromString($dateString, $timezone)); + } + + /** + * Formats date for RSS feeds + * + * @param int|string|DateTime $dateString UNIX timestamp, strtotime() valid string or DateTime object + * @param string|DateTimeZone $timezone Timezone string or DateTimeZone object + * @return string Formatted date string + * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/time.html#TimeHelper::toRSS + */ + public static function toRSS($dateString, $timezone = null) + { + $date = static::fromString($dateString, $timezone); + + if ($timezone === null) { + return date("r", $date); + } + + $userOffset = $timezone; + if (!is_numeric($timezone)) { + if (!is_object($timezone)) { + $timezone = new DateTimeZone($timezone); + } + $currentDate = new DateTime('@' . $date); + $currentDate->setTimezone($timezone); + $userOffset = $timezone->getOffset($currentDate) / 60 / 60; + } + + $timezone = '+0000'; + if ($userOffset != 0) { + $hours = (int)floor(abs($userOffset)); + $minutes = (int)(fmod(abs($userOffset), $hours) * 60); + $timezone = ($userOffset < 0 ? '-' : '+') . str_pad($hours, 2, '0', STR_PAD_LEFT) . str_pad($minutes, 2, '0', STR_PAD_LEFT); + } + return date('D, d M Y H:i:s', $date) . ' ' . $timezone; + } + + /** + * Returns either a relative or a formatted absolute date depending + * on the difference between the current time and given datetime. + * $datetime should be in a *strtotime* - parsable format, like MySQL's datetime datatype. + * + * ### Options: + * + * - `format` => a fall back format if the relative time is longer than the duration specified by end + * - `accuracy` => Specifies how accurate the date should be described (array) + * - year => The format if years > 0 (default "day") + * - month => The format if months > 0 (default "day") + * - week => The format if weeks > 0 (default "day") + * - day => The format if weeks > 0 (default "hour") + * - hour => The format if hours > 0 (default "minute") + * - minute => The format if minutes > 0 (default "minute") + * - second => The format if seconds > 0 (default "second") + * - `end` => The end of relative time telling + * - `relativeString` => The printf compatible string when outputting past relative time + * - `relativeStringFuture` => The printf compatible string when outputting future relative time + * - `absoluteString` => The printf compatible string when outputting absolute time + * - `userOffset` => Users offset from GMT (in hours) *Deprecated* use timezone instead. + * - `timezone` => The user timezone the timestamp should be formatted in. + * + * Relative dates look something like this: + * + * - 3 weeks, 4 days ago + * - 15 seconds ago + * + * Default date formatting is d/m/yy e.g: on 18/2/09 + * + * The returned string includes 'ago' or 'on' and assumes you'll properly add a word + * like 'Posted ' before the function output. + * + * NOTE: If the difference is one week or more, the lowest level of accuracy is day + * + * @param int|string|DateTime $dateTime Datetime UNIX timestamp, strtotime() valid string or DateTime object + * @param array $options Default format if timestamp is used in $dateString + * @return string Relative time string. + * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/time.html#TimeHelper::timeAgoInWords + */ + public static function timeAgoInWords($dateTime, $options = []) + { + $timezone = null; + $accuracies = static::$wordAccuracy; + $format = static::$wordFormat; + $relativeEnd = static::$wordEnd; + $relativeStringPast = __d('cake', '%s ago'); + $relativeStringFuture = __d('cake', 'in %s'); + $absoluteString = __d('cake', 'on %s'); + + if (is_string($options)) { + $format = $options; + } else if (!empty($options)) { + if (isset($options['timezone'])) { + $timezone = $options['timezone']; + } else if (isset($options['userOffset'])) { + $timezone = $options['userOffset']; + } + + if (isset($options['accuracy'])) { + if (is_array($options['accuracy'])) { + $accuracies = array_merge($accuracies, $options['accuracy']); + } else { + foreach ($accuracies as $key => $level) { + $accuracies[$key] = $options['accuracy']; + } + } + } + + if (isset($options['format'])) { + $format = $options['format']; + } + if (isset($options['end'])) { + $relativeEnd = $options['end']; + } + if (isset($options['relativeString'])) { + $relativeStringPast = $options['relativeString']; + unset($options['relativeString']); + } + if (isset($options['relativeStringFuture'])) { + $relativeStringFuture = $options['relativeStringFuture']; + unset($options['relativeStringFuture']); + } + if (isset($options['absoluteString'])) { + $absoluteString = $options['absoluteString']; + unset($options['absoluteString']); + } + unset($options['end'], $options['format']); + } + + $now = static::fromString(time(), $timezone); + $inSeconds = static::fromString($dateTime, $timezone); + $isFuture = ($inSeconds > $now); + + if ($isFuture) { + $startTime = $now; + $endTime = $inSeconds; + } else { + $startTime = $inSeconds; + $endTime = $now; + } + $diff = $endTime - $startTime; + + if ($diff === 0) { + return __d('cake', 'just now', 'just now'); + } + + $isAbsoluteDate = $diff > abs($now - static::fromString($relativeEnd)); + if ($isAbsoluteDate) { + if (strpos($format, '%') === false) { + $date = date($format, $inSeconds); + } else { + $date = static::_strftime($format, $inSeconds); + } + return sprintf($absoluteString, $date); + } + + $years = $months = $weeks = $days = $hours = $minutes = $seconds = 0; + + // If more than a week, then take into account the length of months + if ($diff >= 604800) { + list($future['H'], $future['i'], $future['s'], $future['d'], $future['m'], $future['Y']) = explode('/', date('H/i/s/d/m/Y', $endTime)); + list($past['H'], $past['i'], $past['s'], $past['d'], $past['m'], $past['Y']) = explode('/', date('H/i/s/d/m/Y', $startTime)); + + $years = $future['Y'] - $past['Y']; + $months = $future['m'] + ((12 * $years) - $past['m']); + + if ($months >= 12) { + $years = floor($months / 12); + $months = $months - ($years * 12); + } + if ($future['m'] < $past['m'] && $future['Y'] - $past['Y'] === 1) { + $years--; + } + + if ($future['d'] >= $past['d']) { + $days = $future['d'] - $past['d']; + } else { + $daysInPastMonth = date('t', $startTime); + $daysInFutureMonth = date('t', mktime(0, 0, 0, $future['m'] - 1, 1, $future['Y'])); + + if ($isFuture) { + $days = ($daysInFutureMonth - $past['d']) + $future['d']; + } else { + $days = ($daysInPastMonth - $past['d']) + $future['d']; + } + + if ($future['m'] != $past['m']) { + $months--; + } + } + + if (!$months && $years >= 1 && $diff < ($years * 31536000)) { + $months = 11; + $years--; + } + + if ($months >= 12) { + $years = $years + 1; + $months = $months - 12; + } + + if ($days >= 7) { + $weeks = floor($days / 7); + $days = $days - ($weeks * 7); + } + } else { + $days = floor($diff / 86400); + $diff = $diff - ($days * 86400); + + $hours = floor($diff / 3600); + $diff = $diff - ($hours * 3600); + + $minutes = floor($diff / 60); + $diff = $diff - ($minutes * 60); + + $seconds = $diff; + } + + $accuracy = $accuracies['second']; + if ($years > 0) { + $accuracy = $accuracies['year']; + } else if (abs($months) > 0) { + $accuracy = $accuracies['month']; + } else if (abs($weeks) > 0) { + $accuracy = $accuracies['week']; + } else if (abs($days) > 0) { + $accuracy = $accuracies['day']; + } else if (abs($hours) > 0) { + $accuracy = $accuracies['hour']; + } else if (abs($minutes) > 0) { + $accuracy = $accuracies['minute']; + } + + $accuracyNum = str_replace(['year', 'month', 'week', 'day', 'hour', 'minute', 'second'], [1, 2, 3, 4, 5, 6, 7], $accuracy); + + $relativeDate = []; + if ($accuracyNum >= 1 && $years > 0) { + $relativeDate[] = __dn('cake', '%d year', '%d years', $years, $years); + } + if ($accuracyNum >= 2 && $months > 0) { + $relativeDate[] = __dn('cake', '%d month', '%d months', $months, $months); + } + if ($accuracyNum >= 3 && $weeks > 0) { + $relativeDate[] = __dn('cake', '%d week', '%d weeks', $weeks, $weeks); + } + if ($accuracyNum >= 4 && $days > 0) { + $relativeDate[] = __dn('cake', '%d day', '%d days', $days, $days); + } + if ($accuracyNum >= 5 && $hours > 0) { + $relativeDate[] = __dn('cake', '%d hour', '%d hours', $hours, $hours); + } + if ($accuracyNum >= 6 && $minutes > 0) { + $relativeDate[] = __dn('cake', '%d minute', '%d minutes', $minutes, $minutes); + } + if ($accuracyNum >= 7 && $seconds > 0) { + $relativeDate[] = __dn('cake', '%d second', '%d seconds', $seconds, $seconds); + } + $relativeDate = implode(', ', $relativeDate); + + if ($relativeDate) { + $relativeString = ($isFuture) ? $relativeStringFuture : $relativeStringPast; + return sprintf($relativeString, $relativeDate); + } + + if ($isFuture) { + $strings = [ + 'second' => __d('cake', 'in about a second'), + 'minute' => __d('cake', 'in about a minute'), + 'hour' => __d('cake', 'in about an hour'), + 'day' => __d('cake', 'in about a day'), + 'week' => __d('cake', 'in about a week'), + 'year' => __d('cake', 'in about a year') + ]; + } else { + $strings = [ + 'second' => __d('cake', 'about a second ago'), + 'minute' => __d('cake', 'about a minute ago'), + 'hour' => __d('cake', 'about an hour ago'), + 'day' => __d('cake', 'about a day ago'), + 'week' => __d('cake', 'about a week ago'), + 'year' => __d('cake', 'about a year ago') + ]; + } + + return $strings[$accuracy]; + } + + /** + * Returns gmt as a UNIX timestamp. + * + * @param int|string|DateTime $dateString UNIX timestamp, strtotime() valid string or DateTime object + * @return int UNIX timestamp + * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/time.html#TimeHelper::gmt + */ + public static function gmt($dateString = null) + { + $time = time(); + if ($dateString) { + $time = static::fromString($dateString); + } + return gmmktime( + (int)date('G', $time), + (int)date('i', $time), + (int)date('s', $time), + (int)date('n', $time), + (int)date('j', $time), + (int)date('Y', $time) + ); + } + + /** + * Returns a formatted date string, given either a UNIX timestamp or a valid strtotime() date string. + * This function also accepts a time string and a format string as first and second parameters. + * In that case this function behaves as a wrapper for TimeHelper::i18nFormat() + * + * ## Examples + * + * Create localized & formatted time: + * + * ``` + * CakeTime::format('2012-02-15', '%m-%d-%Y'); // returns 02-15-2012 + * CakeTime::format('2012-02-15 23:01:01', '%c'); // returns preferred date and time based on configured locale + * CakeTime::format('0000-00-00', '%d-%m-%Y', 'N/A'); // return N/A becuase an invalid date was passed + * CakeTime::format('2012-02-15 23:01:01', '%c', 'N/A', 'America/New_York'); // converts passed date to timezone + * ``` + * + * @param int|string|DateTime $date UNIX timestamp, strtotime() valid string or DateTime object (or a date format string) + * @param int|string|DateTime $format date format string (or UNIX timestamp, strtotime() valid string or DateTime object) + * @param bool|string $default if an invalid date is passed it will output supplied default value. Pass false if you want raw conversion value + * @param string|DateTimeZone $timezone Timezone string or DateTimeZone object + * @return string Formatted date string + * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/time.html#TimeHelper::format + * @see CakeTime::i18nFormat() + */ + public static function format($date, $format = null, $default = false, $timezone = null) + { + //Backwards compatible params re-order test + $time = static::fromString($format, $timezone); + + if ($time === false) { + return static::i18nFormat($date, $format, $default, $timezone); + } + return date($date, $time); + } + + /** + * Returns a formatted date string, given either a UNIX timestamp or a valid strtotime() date string. + * It takes into account the default date format for the current language if a LC_TIME file is used. + * + * @param int|string|DateTime $date UNIX timestamp, strtotime() valid string or DateTime object + * @param string $format strftime format string. + * @param bool|string $default if an invalid date is passed it will output supplied default value. Pass false if you want raw conversion value + * @param string|DateTimeZone $timezone Timezone string or DateTimeZone object + * @return string Formatted and translated date string + * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/time.html#TimeHelper::i18nFormat + */ + public static function i18nFormat($date, $format = null, $default = false, $timezone = null) + { + $timestamp = static::fromString($date, $timezone); + if ($timestamp === false && $default !== false) { + return $default; + } + if ($timestamp === false) { + return ''; + } + if (empty($format)) { + $format = '%x'; + } + $convertedFormat = static::convertSpecifiers($format, $timestamp); + return static::_strftimeWithTimezone($convertedFormat, $timestamp, $date, $timezone); + } + + /** + * Get list of timezone identifiers + * + * @param int|string $filter A regex to filter identifier + * Or one of DateTimeZone class constants (PHP 5.3 and above) + * @param string $country A two-letter ISO 3166-1 compatible country code. + * This option is only used when $filter is set to DateTimeZone::PER_COUNTRY (available only in PHP 5.3 and above) + * @param bool|array $options If true (default value) groups the identifiers list by primary region. + * Otherwise, an array containing `group`, `abbr`, `before`, and `after` keys. + * Setting `group` and `abbr` to true will group results and append timezone + * abbreviation in the display value. Set `before` and `after` to customize + * the abbreviation wrapper. + * @return array List of timezone identifiers + * @since 2.2 + * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/time.html#TimeHelper::listTimezones + */ + public static function listTimezones($filter = null, $country = null, $options = []) + { + if (is_bool($options)) { + $options = [ + 'group' => $options, + ]; + } + $defaults = [ + 'group' => true, + 'abbr' => false, + 'before' => ' - ', + 'after' => null, + ]; + $options += $defaults; + $group = $options['group']; + + $regex = null; + if (is_string($filter)) { + $regex = $filter; + $filter = null; + } + if (version_compare(PHP_VERSION, '5.3.0', '<')) { + if ($regex === null) { + $regex = '#^((Africa|America|Antartica|Arctic|Asia|Atlantic|Australia|Europe|Indian|Pacific)/|UTC)#'; + } + $identifiers = DateTimeZone::listIdentifiers(); + } else { + if ($filter === null) { + $filter = DateTimeZone::ALL; + } + $identifiers = DateTimeZone::listIdentifiers($filter, $country); + } + + if ($regex) { + foreach ($identifiers as $key => $tz) { + if (!preg_match($regex, $tz)) { + unset($identifiers[$key]); + } + } + } + + if ($group) { + $return = []; + $now = time(); + $before = $options['before']; + $after = $options['after']; + foreach ($identifiers as $key => $tz) { + $abbr = null; + if ($options['abbr']) { + $dateTimeZone = new DateTimeZone($tz); + $trans = $dateTimeZone->getTransitions($now, $now); + $abbr = isset($trans[0]['abbr']) ? + $before . $trans[0]['abbr'] . $after : + null; + } + $item = explode('/', $tz, 2); + if (isset($item[1])) { + $return[$item[0]][$tz] = $item[1] . $abbr; + } else { + $return[$item[0]] = [$tz => $item[0] . $abbr]; + } + } + return $return; + } + return array_combine($identifiers, $identifiers); + } + + /** + * Auxiliary function to translate a matched specifier element from a regular expression into + * a Windows safe and i18n aware specifier + * + * @param array $specifier match from regular expression + * @return string converted element + */ + protected static function _translateSpecifier($specifier) + { + switch ($specifier[1]) { + case 'a': + $abday = __dc('cake', 'abday', 5); + if (is_array($abday)) { + return $abday[date('w', static::$_time)]; + } + break; + case 'A': + $day = __dc('cake', 'day', 5); + if (is_array($day)) { + return $day[date('w', static::$_time)]; + } + break; + case 'c': + $format = __dc('cake', 'd_t_fmt', 5); + if ($format !== 'd_t_fmt') { + return static::convertSpecifiers($format, static::$_time); + } + break; + case 'C': + return sprintf("%02d", date('Y', static::$_time) / 100); + case 'D': + return '%m/%d/%y'; + case 'e': + if (DS === '/') { + return '%e'; + } + $day = date('j', static::$_time); + if ($day < 10) { + $day = ' ' . $day; + } + return $day; + case 'eS' : + return date('jS', static::$_time); + case 'b': + case 'h': + $months = __dc('cake', 'abmon', 5); + if (is_array($months)) { + return $months[date('n', static::$_time) - 1]; + } + return '%b'; + case 'B': + $months = __dc('cake', 'mon', 5); + if (is_array($months)) { + return $months[date('n', static::$_time) - 1]; + } + break; + case 'n': + return "\n"; + case 'p': + case 'P': + $default = ['am' => 0, 'pm' => 1]; + $meridiem = $default[date('a', static::$_time)]; + $format = __dc('cake', 'am_pm', 5); + if (is_array($format)) { + $meridiem = $format[$meridiem]; + return ($specifier[1] === 'P') ? strtolower($meridiem) : strtoupper($meridiem); + } + break; + case 'r': + $complete = __dc('cake', 't_fmt_ampm', 5); + if ($complete !== 't_fmt_ampm') { + return str_replace('%p', static::_translateSpecifier(['%p', 'p']), $complete); + } + break; + case 'R': + return date('H:i', static::$_time); + case 't': + return "\t"; + case 'T': + return '%H:%M:%S'; + case 'u': + return ($weekDay = date('w', static::$_time)) ? $weekDay : 7; + case 'x': + $format = __dc('cake', 'd_fmt', 5); + if ($format !== 'd_fmt') { + return static::convertSpecifiers($format, static::$_time); + } + break; + case 'X': + $format = __dc('cake', 't_fmt', 5); + if ($format !== 't_fmt') { + return static::convertSpecifiers($format, static::$_time); + } + break; + } + return $specifier[0]; + } + + /** + * Converts a string representing the format for the function strftime and returns a + * Windows safe and i18n aware format. + * + * @param string $format Format with specifiers for strftime function. + * Accepts the special specifier %S which mimics the modifier S for date() + * @param string $time UNIX timestamp + * @return string Windows safe and date() function compatible format for strftime + * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/time.html#TimeHelper::convertSpecifiers + */ + public static function convertSpecifiers($format, $time = null) + { + if (!$time) { + $time = time(); + } + static::$_time = $time; + return preg_replace_callback('/\%(\w+)/', ['CakeTime', '_translateSpecifier'], $format); + } + + /** + * Magic set method for backwards compatibility. + * Used by TimeHelper to get static variables in CakeTime + * + * @param string $name Variable name + * @return mixed + */ + public function __get($name) + { + switch ($name) { + case 'niceFormat': + return static::${$name}; + default: + return null; + } + } + + /** + * Magic set method for backwards compatibility. + * Used by TimeHelper to modify static variables in CakeTime + * + * @param string $name Variable name + * @param mixes $value Variable value + * @return void + */ + public function __set($name, $value) + { + switch ($name) { + case 'niceFormat': + static::${$name} = $value; + break; + } + } } diff --git a/lib/Cake/Utility/ClassRegistry.php b/lib/Cake/Utility/ClassRegistry.php index d7475a72..2e23269c 100755 --- a/lib/Cake/Utility/ClassRegistry.php +++ b/lib/Cake/Utility/ClassRegistry.php @@ -30,336 +30,350 @@ * * @package Cake.Utility */ -class ClassRegistry { +class ClassRegistry +{ -/** - * Names of classes with their objects. - * - * @var array - */ - protected $_objects = array(); + /** + * Names of classes with their objects. + * + * @var array + */ + protected $_objects = []; -/** - * Names of class names mapped to the object in the registry. - * - * @var array - */ - protected $_map = array(); + /** + * Names of class names mapped to the object in the registry. + * + * @var array + */ + protected $_map = []; -/** - * Default constructor parameter settings, indexed by type - * - * @var array - */ - protected $_config = array(); + /** + * Default constructor parameter settings, indexed by type + * + * @var array + */ + protected $_config = []; -/** - * Return a singleton instance of the ClassRegistry. - * - * @return ClassRegistry instance - */ - public static function getInstance() { - static $instance = array(); - if (!$instance) { - $instance[0] = new ClassRegistry(); - } - return $instance[0]; - } + /** + * Loads a class, registers the object in the registry and returns instance of the object. ClassRegistry::init() + * is used as a factory for models, and handle correct injecting of settings, that assist in testing. + * + * Examples + * Simple Use: Get a Post model instance ```ClassRegistry::init('Post');``` + * + * Expanded: ```array('class' => 'ClassName', 'alias' => 'AliasNameStoredInTheRegistry');``` + * + * Model Classes can accept optional ```array('id' => $id, 'table' => $table, 'ds' => $ds, 'alias' => $alias);``` + * + * When $class is a numeric keyed array, multiple class instances will be stored in the registry, + * no instance of the object will be returned + * ``` + * array( + * array('class' => 'ClassName', 'alias' => 'AliasNameStoredInTheRegistry'), + * array('class' => 'ClassName', 'alias' => 'AliasNameStoredInTheRegistry'), + * array('class' => 'ClassName', 'alias' => 'AliasNameStoredInTheRegistry') + * ); + * ``` + * + * @param string|array $class as a string or a single key => value array instance will be created, + * stored in the registry and returned. + * @param bool $strict if set to true it will return false if the class was not found instead + * of trying to create an AppModel + * @return bool|object $class instance of ClassName. + * @throws CakeException when you try to construct an interface or abstract class. + */ + public static function init($class, $strict = false) + { + $_this = ClassRegistry::getInstance(); -/** - * Loads a class, registers the object in the registry and returns instance of the object. ClassRegistry::init() - * is used as a factory for models, and handle correct injecting of settings, that assist in testing. - * - * Examples - * Simple Use: Get a Post model instance ```ClassRegistry::init('Post');``` - * - * Expanded: ```array('class' => 'ClassName', 'alias' => 'AliasNameStoredInTheRegistry');``` - * - * Model Classes can accept optional ```array('id' => $id, 'table' => $table, 'ds' => $ds, 'alias' => $alias);``` - * - * When $class is a numeric keyed array, multiple class instances will be stored in the registry, - * no instance of the object will be returned - * ``` - * array( - * array('class' => 'ClassName', 'alias' => 'AliasNameStoredInTheRegistry'), - * array('class' => 'ClassName', 'alias' => 'AliasNameStoredInTheRegistry'), - * array('class' => 'ClassName', 'alias' => 'AliasNameStoredInTheRegistry') - * ); - * ``` - * - * @param string|array $class as a string or a single key => value array instance will be created, - * stored in the registry and returned. - * @param bool $strict if set to true it will return false if the class was not found instead - * of trying to create an AppModel - * @return bool|object $class instance of ClassName. - * @throws CakeException when you try to construct an interface or abstract class. - */ - public static function init($class, $strict = false) { - $_this = ClassRegistry::getInstance(); + if (is_array($class)) { + $objects = $class; + if (!isset($class[0])) { + $objects = [$class]; + } + } else { + $objects = [['class' => $class]]; + } + $defaults = []; + if (isset($_this->_config['Model'])) { + $defaults = $_this->_config['Model']; + } + $count = count($objects); + $availableDs = null; - if (is_array($class)) { - $objects = $class; - if (!isset($class[0])) { - $objects = array($class); - } - } else { - $objects = array(array('class' => $class)); - } - $defaults = array(); - if (isset($_this->_config['Model'])) { - $defaults = $_this->_config['Model']; - } - $count = count($objects); - $availableDs = null; + foreach ($objects as $settings) { + if (is_numeric($settings)) { + trigger_error(__d('cake_dev', '(ClassRegistry::init() Attempted to create instance of a class with a numeric name'), E_USER_WARNING); + return false; + } - foreach ($objects as $settings) { - if (is_numeric($settings)) { - trigger_error(__d('cake_dev', '(ClassRegistry::init() Attempted to create instance of a class with a numeric name'), E_USER_WARNING); - return false; - } + if (is_array($settings)) { + $pluginPath = null; + $settings += $defaults; + $class = $settings['class']; - if (is_array($settings)) { - $pluginPath = null; - $settings += $defaults; - $class = $settings['class']; + list($plugin, $class) = pluginSplit($class); + if ($plugin) { + $pluginPath = $plugin . '.'; + $settings['plugin'] = $plugin; + } - list($plugin, $class) = pluginSplit($class); - if ($plugin) { - $pluginPath = $plugin . '.'; - $settings['plugin'] = $plugin; - } + if (empty($settings['alias'])) { + $settings['alias'] = $class; + } + $alias = $settings['alias']; - if (empty($settings['alias'])) { - $settings['alias'] = $class; - } - $alias = $settings['alias']; + $model = $_this->_duplicate($alias, $class); + if ($model) { + $_this->map($alias, $class); + return $model; + } - $model = $_this->_duplicate($alias, $class); - if ($model) { - $_this->map($alias, $class); - return $model; - } + App::uses($plugin . 'AppModel', $pluginPath . 'Model'); + App::uses($class, $pluginPath . 'Model'); - App::uses($plugin . 'AppModel', $pluginPath . 'Model'); - App::uses($class, $pluginPath . 'Model'); + if (class_exists($class) || interface_exists($class)) { + $reflection = new ReflectionClass($class); + if ($reflection->isAbstract() || $reflection->isInterface()) { + throw new CakeException(__d('cake_dev', 'Cannot create instance of %s, as it is abstract or is an interface', $class)); + } + $testing = isset($settings['testing']) ? $settings['testing'] : false; + if ($testing) { + $settings['ds'] = 'test'; + $defaultProperties = $reflection->getDefaultProperties(); + if (isset($defaultProperties['useDbConfig'])) { + $useDbConfig = $defaultProperties['useDbConfig']; + if ($availableDs === null) { + $availableDs = array_keys(ConnectionManager::enumConnectionObjects()); + } + if (in_array('test_' . $useDbConfig, $availableDs)) { + $useDbConfig = 'test_' . $useDbConfig; + } + if (strpos($useDbConfig, 'test') === 0) { + $settings['ds'] = $useDbConfig; + } + } + } + if ($reflection->getConstructor()) { + $instance = $reflection->newInstance($settings); + } else { + $instance = $reflection->newInstance(); + } + if ($strict && !$instance instanceof Model) { + $instance = null; + } + } + if (!isset($instance)) { + $appModel = 'AppModel'; + if ($strict) { + return false; + } else if ($plugin && class_exists($plugin . 'AppModel')) { + $appModel = $plugin . 'AppModel'; + } - if (class_exists($class) || interface_exists($class)) { - $reflection = new ReflectionClass($class); - if ($reflection->isAbstract() || $reflection->isInterface()) { - throw new CakeException(__d('cake_dev', 'Cannot create instance of %s, as it is abstract or is an interface', $class)); - } - $testing = isset($settings['testing']) ? $settings['testing'] : false; - if ($testing) { - $settings['ds'] = 'test'; - $defaultProperties = $reflection->getDefaultProperties(); - if (isset($defaultProperties['useDbConfig'])) { - $useDbConfig = $defaultProperties['useDbConfig']; - if ($availableDs === null) { - $availableDs = array_keys(ConnectionManager::enumConnectionObjects()); - } - if (in_array('test_' . $useDbConfig, $availableDs)) { - $useDbConfig = 'test_' . $useDbConfig; - } - if (strpos($useDbConfig, 'test') === 0) { - $settings['ds'] = $useDbConfig; - } - } - } - if ($reflection->getConstructor()) { - $instance = $reflection->newInstance($settings); - } else { - $instance = $reflection->newInstance(); - } - if ($strict && !$instance instanceof Model) { - $instance = null; - } - } - if (!isset($instance)) { - $appModel = 'AppModel'; - if ($strict) { - return false; - } elseif ($plugin && class_exists($plugin . 'AppModel')) { - $appModel = $plugin . 'AppModel'; - } + $settings['name'] = $class; + $instance = new $appModel($settings); + } + $_this->map($alias, $class); + } + } - $settings['name'] = $class; - $instance = new $appModel($settings); - } - $_this->map($alias, $class); - } - } + if ($count > 1) { + return true; + } + return $instance; + } - if ($count > 1) { - return true; - } - return $instance; - } + /** + * Return a singleton instance of the ClassRegistry. + * + * @return ClassRegistry instance + */ + public static function getInstance() + { + static $instance = []; + if (!$instance) { + $instance[0] = new ClassRegistry(); + } + return $instance[0]; + } -/** - * Add $object to the registry, associating it with the name $key. - * - * @param string $key Key for the object in registry - * @param object $object Object to store - * @return bool True if the object was written, false if $key already exists - */ - public static function addObject($key, $object) { - $_this = ClassRegistry::getInstance(); - $key = Inflector::underscore($key); - if (!isset($_this->_objects[$key])) { - $_this->_objects[$key] = $object; - return true; - } - return false; - } + /** + * Checks to see if $alias is a duplicate $class Object + * + * @param string $alias Alias to check. + * @param string $class Class name. + * @return bool|object Object stored in registry or `false` if the object does not exist. + */ + protected function &_duplicate($alias, $class) + { + $duplicate = false; + if ($this->isKeySet($alias)) { + $model = $this->getObject($alias); + if (is_object($model) && ($model instanceof $class || $model->alias === $class)) { + $duplicate = $model; + } + unset($model); + } + return $duplicate; + } -/** - * Remove object which corresponds to given key. - * - * @param string $key Key of object to remove from registry - * @return void - */ - public static function removeObject($key) { - $_this = ClassRegistry::getInstance(); - $key = Inflector::underscore($key); - if (isset($_this->_objects[$key])) { - unset($_this->_objects[$key]); - } - } + /** + * Returns true if given key is present in the ClassRegistry. + * + * @param string $key Key to look for + * @return bool true if key exists in registry, false otherwise + */ + public static function isKeySet($key) + { + $_this = ClassRegistry::getInstance(); + $key = Inflector::underscore($key); -/** - * Returns true if given key is present in the ClassRegistry. - * - * @param string $key Key to look for - * @return bool true if key exists in registry, false otherwise - */ - public static function isKeySet($key) { - $_this = ClassRegistry::getInstance(); - $key = Inflector::underscore($key); + return isset($_this->_objects[$key]) || isset($_this->_map[$key]); + } - return isset($_this->_objects[$key]) || isset($_this->_map[$key]); - } + /** + * Return object which corresponds to given key. + * + * @param string $key Key of object to look for + * @return mixed Object stored in registry or boolean false if the object does not exist. + */ + public static function getObject($key) + { + $_this = ClassRegistry::getInstance(); + $key = Inflector::underscore($key); + $return = false; + if (isset($_this->_objects[$key])) { + $return = $_this->_objects[$key]; + } else { + $key = $_this->_getMap($key); + if (isset($_this->_objects[$key])) { + $return = $_this->_objects[$key]; + } + } + return $return; + } -/** - * Get all keys from the registry. - * - * @return array Set of keys stored in registry - */ - public static function keys() { - return array_keys(ClassRegistry::getInstance()->_objects); - } + /** + * Return the name of a class in the registry. + * + * @param string $key Key to find in map + * @return string Mapped value + */ + protected function _getMap($key) + { + if (isset($this->_map[$key])) { + return $this->_map[$key]; + } + } -/** - * Return object which corresponds to given key. - * - * @param string $key Key of object to look for - * @return mixed Object stored in registry or boolean false if the object does not exist. - */ - public static function getObject($key) { - $_this = ClassRegistry::getInstance(); - $key = Inflector::underscore($key); - $return = false; - if (isset($_this->_objects[$key])) { - $return = $_this->_objects[$key]; - } else { - $key = $_this->_getMap($key); - if (isset($_this->_objects[$key])) { - $return = $_this->_objects[$key]; - } - } - return $return; - } + /** + * Add a key name pair to the registry to map name to class in the registry. + * + * @param string $key Key to include in map + * @param string $name Key that is being mapped + * @return void + */ + public static function map($key, $name) + { + $_this = ClassRegistry::getInstance(); + $key = Inflector::underscore($key); + $name = Inflector::underscore($name); + if (!isset($_this->_map[$key])) { + $_this->_map[$key] = $name; + } + } -/** - * Sets the default constructor parameter for an object type - * - * @param string $type Type of object. If this parameter is omitted, defaults to "Model" - * @param array $param The parameter that will be passed to object constructors when objects - * of $type are created - * @return mixed Void if $param is being set. Otherwise, if only $type is passed, returns - * the previously-set value of $param, or null if not set. - */ - public static function config($type, $param = array()) { - $_this = ClassRegistry::getInstance(); + /** + * Add $object to the registry, associating it with the name $key. + * + * @param string $key Key for the object in registry + * @param object $object Object to store + * @return bool True if the object was written, false if $key already exists + */ + public static function addObject($key, $object) + { + $_this = ClassRegistry::getInstance(); + $key = Inflector::underscore($key); + if (!isset($_this->_objects[$key])) { + $_this->_objects[$key] = $object; + return true; + } + return false; + } - if (empty($param) && is_array($type)) { - $param = $type; - $type = 'Model'; - } elseif ($param === null) { - unset($_this->_config[$type]); - } elseif (empty($param) && is_string($type)) { - return isset($_this->_config[$type]) ? $_this->_config[$type] : null; - } - if (isset($_this->_config[$type]['testing'])) { - $param['testing'] = true; - } - $_this->_config[$type] = $param; - } + /** + * Remove object which corresponds to given key. + * + * @param string $key Key of object to remove from registry + * @return void + */ + public static function removeObject($key) + { + $_this = ClassRegistry::getInstance(); + $key = Inflector::underscore($key); + if (isset($_this->_objects[$key])) { + unset($_this->_objects[$key]); + } + } -/** - * Checks to see if $alias is a duplicate $class Object - * - * @param string $alias Alias to check. - * @param string $class Class name. - * @return bool|object Object stored in registry or `false` if the object does not exist. - */ - protected function &_duplicate($alias, $class) { - $duplicate = false; - if ($this->isKeySet($alias)) { - $model = $this->getObject($alias); - if (is_object($model) && ($model instanceof $class || $model->alias === $class)) { - $duplicate = $model; - } - unset($model); - } - return $duplicate; - } + /** + * Get all keys from the registry. + * + * @return array Set of keys stored in registry + */ + public static function keys() + { + return array_keys(ClassRegistry::getInstance()->_objects); + } -/** - * Add a key name pair to the registry to map name to class in the registry. - * - * @param string $key Key to include in map - * @param string $name Key that is being mapped - * @return void - */ - public static function map($key, $name) { - $_this = ClassRegistry::getInstance(); - $key = Inflector::underscore($key); - $name = Inflector::underscore($name); - if (!isset($_this->_map[$key])) { - $_this->_map[$key] = $name; - } - } + /** + * Sets the default constructor parameter for an object type + * + * @param string $type Type of object. If this parameter is omitted, defaults to "Model" + * @param array $param The parameter that will be passed to object constructors when objects + * of $type are created + * @return mixed Void if $param is being set. Otherwise, if only $type is passed, returns + * the previously-set value of $param, or null if not set. + */ + public static function config($type, $param = []) + { + $_this = ClassRegistry::getInstance(); -/** - * Get all keys from the map in the registry. - * - * @return array Keys of registry's map - */ - public static function mapKeys() { - return array_keys(ClassRegistry::getInstance()->_map); - } + if (empty($param) && is_array($type)) { + $param = $type; + $type = 'Model'; + } else if ($param === null) { + unset($_this->_config[$type]); + } else if (empty($param) && is_string($type)) { + return isset($_this->_config[$type]) ? $_this->_config[$type] : null; + } + if (isset($_this->_config[$type]['testing'])) { + $param['testing'] = true; + } + $_this->_config[$type] = $param; + } -/** - * Return the name of a class in the registry. - * - * @param string $key Key to find in map - * @return string Mapped value - */ - protected function _getMap($key) { - if (isset($this->_map[$key])) { - return $this->_map[$key]; - } - } + /** + * Get all keys from the map in the registry. + * + * @return array Keys of registry's map + */ + public static function mapKeys() + { + return array_keys(ClassRegistry::getInstance()->_map); + } -/** - * Flushes all objects from the ClassRegistry. - * - * @return void - */ - public static function flush() { - $_this = ClassRegistry::getInstance(); - $_this->_objects = array(); - $_this->_map = array(); - } + /** + * Flushes all objects from the ClassRegistry. + * + * @return void + */ + public static function flush() + { + $_this = ClassRegistry::getInstance(); + $_this->_objects = []; + $_this->_map = []; + } } diff --git a/lib/Cake/Utility/Debugger.php b/lib/Cake/Utility/Debugger.php index ef906659..283c2414 100755 --- a/lib/Cake/Utility/Debugger.php +++ b/lib/Cake/Utility/Debugger.php @@ -29,822 +29,842 @@ * @package Cake.Utility * @link https://book.cakephp.org/2.0/en/development/debugging.html#debugger-class */ -class Debugger { - -/** - * A list of errors generated by the application. - * - * @var array - */ - public $errors = array(); - -/** - * The current output format. - * - * @var string - */ - protected $_outputFormat = 'js'; - -/** - * Templates used when generating trace or error strings. Can be global or indexed by the format - * value used in $_outputFormat. - * - * @var string - */ - protected $_templates = array( - 'log' => array( - 'trace' => '{:reference} - {:path}, line {:line}', - 'error' => "{:error} ({:code}): {:description} in [{:file}, line {:line}]" - ), - 'js' => array( - 'error' => '', - 'info' => '', - 'trace' => '
{:trace}
', - 'code' => '', - 'context' => '', - 'links' => array(), - 'escapeContext' => true, - ), - 'html' => array( - 'trace' => '
Trace 

{:trace}

', - 'context' => '
Context 

{:context}

', - 'escapeContext' => true, - ), - 'txt' => array( - 'error' => "{:error}: {:code} :: {:description} on line {:line} of {:path}\n{:info}", - 'code' => '', - 'info' => '' - ), - 'base' => array( - 'traceLine' => '{:reference} - {:path}, line {:line}', - 'trace' => "Trace:\n{:trace}\n", - 'context' => "Context:\n{:context}\n", - ) - ); - -/** - * Holds current output data when outputFormat is false. - * - * @var string - */ - protected $_data = array(); - -/** - * Constructor. - */ - public function __construct() { - $docRef = ini_get('docref_root'); - - if (empty($docRef) && function_exists('ini_set')) { - ini_set('docref_root', 'http://php.net/'); - } - if (!defined('E_RECOVERABLE_ERROR')) { - define('E_RECOVERABLE_ERROR', 4096); - } - - $e = '
';
-		$e .= '{:error} ({:code}): {:description} ';
-		$e .= '[{:path}, line {:line}]';
-
-		$e .= '';
-		$e .= '
'; - $this->_templates['js']['error'] = $e; - - $t = ''; - $this->_templates['js']['info'] = $t; - - $links = array(); - $link = 'Code'; - $links['code'] = $link; - - $link = 'Context'; - $links['context'] = $link; - - $this->_templates['js']['links'] = $links; - - $this->_templates['js']['context'] = '
_templates['js']['context'] .= 'style="display: none;">{:context}
'; - - $this->_templates['js']['code'] = '
_templates['js']['code'] .= 'style="display: none;">{:code}
'; - - $e = '
{:error} ({:code}) : {:description} ';
-		$e .= '[{:path}, line {:line}]
'; - $this->_templates['html']['error'] = $e; - - $this->_templates['html']['context'] = '
Context ';
-		$this->_templates['html']['context'] .= '

{:context}

'; - } - -/** - * Returns a reference to the Debugger singleton object instance. - * - * @param string $class Debugger class name. - * @return object - */ - public static function getInstance($class = null) { - static $instance = array(); - if (!empty($class)) { - if (!$instance || strtolower($class) != strtolower(get_class($instance[0]))) { - $instance[0] = new $class(); - } - } - if (!$instance) { - $instance[0] = new Debugger(); - } - return $instance[0]; - } - -/** - * Recursively formats and outputs the contents of the supplied variable. - * - * @param mixed $var the variable to dump - * @param int $depth The depth to output to. Defaults to 3. - * @return void - * @see Debugger::exportVar() - * @link https://book.cakephp.org/2.0/en/development/debugging.html#Debugger::dump - */ - public static function dump($var, $depth = 3) { - pr(static::exportVar($var, $depth)); - } - -/** - * Creates an entry in the log file. The log entry will contain a stack trace from where it was called. - * as well as export the variable using exportVar. By default the log is written to the debug log. - * - * @param mixed $var Variable or content to log - * @param int|string $level Type of log to use. Defaults to LOG_DEBUG. When value is an integer - * or a string matching the recognized levels, then it will - * be treated as a log level. Otherwise it's treated as a scope. - * @param int $depth The depth to output to. Defaults to 3. - * @return void - * @link https://book.cakephp.org/2.0/en/development/debugging.html#Debugger::log - */ - public static function log($var, $level = LOG_DEBUG, $depth = 3) { - $source = static::trace(array('start' => 1)) . "\n"; - CakeLog::write($level, "\n" . $source . static::exportVar($var, $depth)); - } - -/** - * Overrides PHP's default error handling. - * - * @param int $code Code of error - * @param string $description Error description - * @param string $file File on which error occurred - * @param int $line Line that triggered the error - * @param array $context Context - * @return bool|null True if error was handled, otherwise null. - * @deprecated 3.0.0 Will be removed in 3.0. This function is superseded by Debugger::outputError(). - */ - public static function showError($code, $description, $file = null, $line = null, $context = null) { - $self = Debugger::getInstance(); - - if (empty($file)) { - $file = '[internal]'; - } - if (empty($line)) { - $line = '??'; - } - - $info = compact('code', 'description', 'file', 'line'); - if (!in_array($info, $self->errors)) { - $self->errors[] = $info; - } else { - return null; - } - - switch ($code) { - case E_PARSE: - case E_ERROR: - case E_CORE_ERROR: - case E_COMPILE_ERROR: - case E_USER_ERROR: - $error = 'Fatal Error'; - $level = LOG_ERR; - break; - case E_WARNING: - case E_USER_WARNING: - case E_COMPILE_WARNING: - case E_RECOVERABLE_ERROR: - $error = 'Warning'; - $level = LOG_WARNING; - break; - case E_NOTICE: - case E_USER_NOTICE: - $error = 'Notice'; - $level = LOG_NOTICE; - break; - case E_DEPRECATED: - case E_USER_DEPRECATED: - $error = 'Deprecated'; - $level = LOG_NOTICE; - break; - default: - return null; - } - - $data = compact( - 'level', 'error', 'code', 'description', 'file', 'line', 'context' - ); - echo $self->outputError($data); - - if ($error === 'Fatal Error') { - exit(); - } - return true; - } - -/** - * Outputs a stack trace based on the supplied options. - * - * ### Options - * - * - `depth` - The number of stack frames to return. Defaults to 999 - * - `format` - The format you want the return. Defaults to the currently selected format. If - * format is 'array' or 'points' the return will be an array. - * - `args` - Should arguments for functions be shown? If true, the arguments for each method call - * will be displayed. - * - `start` - The stack frame to start generating a trace from. Defaults to 0 - * - * @param array $options Format for outputting stack trace - * @return mixed Formatted stack trace - * @link https://book.cakephp.org/2.0/en/development/debugging.html#Debugger::trace - */ - public static function trace($options = array()) { - $self = Debugger::getInstance(); - $defaults = array( - 'depth' => 999, - 'format' => $self->_outputFormat, - 'args' => false, - 'start' => 0, - 'scope' => null, - 'exclude' => array('call_user_func_array', 'trigger_error') - ); - $options = Hash::merge($defaults, $options); - - $backtrace = debug_backtrace(); - $count = count($backtrace); - $back = array(); - - $_trace = array( - 'line' => '??', - 'file' => '[internal]', - 'class' => null, - 'function' => '[main]' - ); - - for ($i = $options['start']; $i < $count && $i < $options['depth']; $i++) { - $trace = array_merge(array('file' => '[internal]', 'line' => '??'), $backtrace[$i]); - $signature = $reference = '[main]'; - - if (isset($backtrace[$i + 1])) { - $next = array_merge($_trace, $backtrace[$i + 1]); - $signature = $reference = $next['function']; - - if (!empty($next['class'])) { - $signature = $next['class'] . '::' . $next['function']; - $reference = $signature . '('; - if ($options['args'] && isset($next['args'])) { - $args = array(); - foreach ($next['args'] as $arg) { - $args[] = Debugger::exportVar($arg); - } - $reference .= implode(', ', $args); - } - $reference .= ')'; - } - } - if (in_array($signature, $options['exclude'])) { - continue; - } - if ($options['format'] === 'points' && $trace['file'] !== '[internal]') { - $back[] = array('file' => $trace['file'], 'line' => $trace['line']); - } elseif ($options['format'] === 'array') { - $back[] = $trace; - } else { - if (isset($self->_templates[$options['format']]['traceLine'])) { - $tpl = $self->_templates[$options['format']]['traceLine']; - } else { - $tpl = $self->_templates['base']['traceLine']; - } - $trace['path'] = static::trimPath($trace['file']); - $trace['reference'] = $reference; - unset($trace['object'], $trace['args']); - $back[] = CakeText::insert($tpl, $trace, array('before' => '{:', 'after' => '}')); - } - } - - if ($options['format'] === 'array' || $options['format'] === 'points') { - return $back; - } - return implode("\n", $back); - } - -/** - * Shortens file paths by replacing the application base path with 'APP', and the CakePHP core - * path with 'CORE'. - * - * @param string $path Path to shorten - * @return string Normalized path - */ - public static function trimPath($path) { - if (!defined('CAKE_CORE_INCLUDE_PATH') || !defined('APP')) { - return $path; - } - - if (strpos($path, APP) === 0) { - return str_replace(APP, 'APP' . DS, $path); - } elseif (strpos($path, CAKE_CORE_INCLUDE_PATH) === 0) { - return str_replace(CAKE_CORE_INCLUDE_PATH, 'CORE', $path); - } elseif (strpos($path, ROOT) === 0) { - return str_replace(ROOT, 'ROOT', $path); - } - - return $path; - } - -/** - * Grabs an excerpt from a file and highlights a given line of code. - * - * Usage: - * - * `Debugger::excerpt('/path/to/file', 100, 4);` - * - * The above would return an array of 8 items. The 4th item would be the provided line, - * and would be wrapped in ``. All of the lines - * are processed with highlight_string() as well, so they have basic PHP syntax highlighting - * applied. - * - * @param string $file Absolute path to a PHP file - * @param int $line Line number to highlight - * @param int $context Number of lines of context to extract above and below $line - * @return array Set of lines highlighted - * @see http://php.net/highlight_string - * @link https://book.cakephp.org/2.0/en/development/debugging.html#Debugger::excerpt - */ - public static function excerpt($file, $line, $context = 2) { - $lines = array(); - if (!file_exists($file)) { - return array(); - } - $data = file_get_contents($file); - if (empty($data)) { - return $lines; - } - if (strpos($data, "\n") !== false) { - $data = explode("\n", $data); - } - if (!isset($data[$line])) { - return $lines; - } - for ($i = $line - ($context + 1); $i < $line + $context; $i++) { - if (!isset($data[$i])) { - continue; - } - $string = str_replace(array("\r\n", "\n"), "", static::_highlight($data[$i])); - if ($i == $line) { - $lines[] = '' . $string . ''; - } else { - $lines[] = $string; - } - } - return $lines; - } - -/** - * Wraps the highlight_string function in case the server API does not - * implement the function as it is the case of the HipHop interpreter - * - * @param string $str the string to convert - * @return string - */ - protected static function _highlight($str) { - if (function_exists('hphp_log') || function_exists('hphp_gettid')) { - return htmlentities($str); - } - $added = false; - if (strpos($str, '', - '', - $highlight - ); - } - return $highlight; - } - -/** - * Converts a variable to a string for debug output. - * - * *Note:* The following keys will have their contents - * replaced with `*****`: - * - * - password - * - login - * - host - * - database - * - port - * - * This is done to protect database credentials, which could be accidentally - * shown in an error message if CakePHP is deployed in development mode. - * - * @param string $var Variable to convert - * @param int $depth The depth to output to. Defaults to 3. - * @return string Variable as a formatted string - * @link https://book.cakephp.org/2.0/en/development/debugging.html#Debugger::exportVar - */ - public static function exportVar($var, $depth = 3) { - return static::_export($var, $depth, 0); - } - -/** - * Protected export function used to keep track of indentation and recursion. - * - * @param mixed $var The variable to dump. - * @param int $depth The remaining depth. - * @param int $indent The current indentation level. - * @return string The dumped variable. - */ - protected static function _export($var, $depth, $indent) { - switch (static::getType($var)) { - case 'boolean': - return ($var) ? 'true' : 'false'; - case 'integer': - return '(int) ' . $var; - case 'float': - return '(float) ' . $var; - case 'string': - if (trim($var) === '') { - return "''"; - } - return "'" . $var . "'"; - case 'array': - return static::_array($var, $depth - 1, $indent + 1); - case 'resource': - return strtolower(gettype($var)); - case 'null': - return 'null'; - case 'unknown': - return 'unknown'; - default: - return static::_object($var, $depth - 1, $indent + 1); - } - } - -/** - * Export an array type object. Filters out keys used in datasource configuration. - * - * The following keys are replaced with ***'s - * - * - password - * - login - * - host - * - database - * - port - * - * @param array $var The array to export. - * @param int $depth The current depth, used for recursion tracking. - * @param int $indent The current indentation level. - * @return string Exported array. - */ - protected static function _array(array $var, $depth, $indent) { - $secrets = array( - 'password' => '*****', - 'login' => '*****', - 'host' => '*****', - 'database' => '*****', - 'port' => '*****' - ); - $replace = array_intersect_key($secrets, $var); - $var = $replace + $var; - - $out = "array("; - $break = $end = null; - if (!empty($var)) { - $break = "\n" . str_repeat("\t", $indent); - $end = "\n" . str_repeat("\t", $indent - 1); - } - $vars = array(); - - if ($depth >= 0) { - foreach ($var as $key => $val) { - // Sniff for globals as !== explodes in < 5.4 - if ($key === 'GLOBALS' && is_array($val) && isset($val['GLOBALS'])) { - $val = '[recursion]'; - } elseif ($val !== $var) { - $val = static::_export($val, $depth, $indent); - } - $vars[] = $break . static::exportVar($key) . - ' => ' . - $val; - } - } else { - $vars[] = $break . '[maximum depth reached]'; - } - return $out . implode(',', $vars) . $end . ')'; - } - -/** - * Handles object to string conversion. - * - * @param string $var Object to convert - * @param int $depth The current depth, used for tracking recursion. - * @param int $indent The current indentation level. - * @return string - * @see Debugger::exportVar() - */ - protected static function _object($var, $depth, $indent) { - $out = ''; - $props = array(); - - $className = get_class($var); - $out .= 'object(' . $className . ') {'; - - if ($depth > 0) { - $end = "\n" . str_repeat("\t", $indent - 1); - $break = "\n" . str_repeat("\t", $indent); - $objectVars = get_object_vars($var); - foreach ($objectVars as $key => $value) { - $value = static::_export($value, $depth - 1, $indent); - $props[] = "$key => " . $value; - } - - if (version_compare(PHP_VERSION, '5.3.0') >= 0) { - $ref = new ReflectionObject($var); - - $filters = array( - ReflectionProperty::IS_PROTECTED => 'protected', - ReflectionProperty::IS_PRIVATE => 'private', - ); - foreach ($filters as $filter => $visibility) { - $reflectionProperties = $ref->getProperties($filter); - foreach ($reflectionProperties as $reflectionProperty) { - $reflectionProperty->setAccessible(true); - $property = $reflectionProperty->getValue($var); - - $value = static::_export($property, $depth - 1, $indent); - $key = $reflectionProperty->name; - $props[] = sprintf('[%s] %s => %s', $visibility, $key, $value); - } - } - } - - $out .= $break . implode($break, $props) . $end; - } - $out .= '}'; - return $out; - } - -/** - * Get/Set the output format for Debugger error rendering. - * - * @param string $format The format you want errors to be output as. - * Leave null to get the current format. - * @return mixed Returns null when setting. Returns the current format when getting. - * @throws CakeException when choosing a format that doesn't exist. - */ - public static function outputAs($format = null) { - $self = Debugger::getInstance(); - if ($format === null) { - return $self->_outputFormat; - } - if ($format !== false && !isset($self->_templates[$format])) { - throw new CakeException(__d('cake_dev', 'Invalid Debugger output format.')); - } - $self->_outputFormat = $format; - } - -/** - * Add an output format or update a format in Debugger. - * - * `Debugger::addFormat('custom', $data);` - * - * Where $data is an array of strings that use CakeText::insert() variable - * replacement. The template vars should be in a `{:id}` style. - * An error formatter can have the following keys: - * - * - 'error' - Used for the container for the error message. Gets the following template - * variables: `id`, `error`, `code`, `description`, `path`, `line`, `links`, `info` - * - 'info' - A combination of `code`, `context` and `trace`. Will be set with - * the contents of the other template keys. - * - 'trace' - The container for a stack trace. Gets the following template - * variables: `trace` - * - 'context' - The container element for the context variables. - * Gets the following templates: `id`, `context` - * - 'links' - An array of HTML links that are used for creating links to other resources. - * Typically this is used to create javascript links to open other sections. - * Link keys, are: `code`, `context`, `help`. See the js output format for an - * example. - * - 'traceLine' - Used for creating lines in the stacktrace. Gets the following - * template variables: `reference`, `path`, `line` - * - * Alternatively if you want to use a custom callback to do all the formatting, you can use - * the callback key, and provide a callable: - * - * `Debugger::addFormat('custom', array('callback' => array($foo, 'outputError'));` - * - * The callback can expect two parameters. The first is an array of all - * the error data. The second contains the formatted strings generated using - * the other template strings. Keys like `info`, `links`, `code`, `context` and `trace` - * will be present depending on the other templates in the format type. - * - * @param string $format Format to use, including 'js' for JavaScript-enhanced HTML, 'html' for - * straight HTML output, or 'txt' for unformatted text. - * @param array $strings Template strings, or a callback to be used for the output format. - * @return The resulting format string set. - */ - public static function addFormat($format, array $strings) { - $self = Debugger::getInstance(); - if (isset($self->_templates[$format])) { - if (isset($strings['links'])) { - $self->_templates[$format]['links'] = array_merge( - $self->_templates[$format]['links'], - $strings['links'] - ); - unset($strings['links']); - } - $self->_templates[$format] = array_merge($self->_templates[$format], $strings); - } else { - $self->_templates[$format] = $strings; - } - return $self->_templates[$format]; - } - -/** - * Switches output format, updates format strings. - * Can be used to switch the active output format: - * - * @param string $format Format to use, including 'js' for JavaScript-enhanced HTML, 'html' for - * straight HTML output, or 'txt' for unformatted text. - * @param array $strings Template strings to be used for the output format. - * @return string - * @deprecated 3.0.0 Use Debugger::outputAs() and Debugger::addFormat(). Will be removed - * in 3.0 - */ - public static function output($format = null, $strings = array()) { - $self = Debugger::getInstance(); - $data = null; - - if ($format === null) { - return Debugger::outputAs(); - } - - if (!empty($strings)) { - return Debugger::addFormat($format, $strings); - } - - if ($format === true && !empty($self->_data)) { - $data = $self->_data; - $self->_data = array(); - $format = false; - } - Debugger::outputAs($format); - return $data; - } - -/** - * Takes a processed array of data from an error and displays it in the chosen format. - * - * @param string $data Data to output. - * @return void - */ - public function outputError($data) { - $defaults = array( - 'level' => 0, - 'error' => 0, - 'code' => 0, - 'description' => '', - 'file' => '', - 'line' => 0, - 'context' => array(), - 'start' => 2, - ); - $data += $defaults; - - $files = $this->trace(array('start' => $data['start'], 'format' => 'points')); - $code = ''; - $file = null; - if (isset($files[0]['file'])) { - $file = $files[0]; - } elseif (isset($files[1]['file'])) { - $file = $files[1]; - } - if ($file) { - $code = $this->excerpt($file['file'], $file['line'] - 1, 1); - } - $trace = $this->trace(array('start' => $data['start'], 'depth' => '20')); - $insertOpts = array('before' => '{:', 'after' => '}'); - $context = array(); - $links = array(); - $info = ''; - - foreach ((array)$data['context'] as $var => $value) { - $context[] = "\${$var} = " . $this->exportVar($value, 3); - } - - switch ($this->_outputFormat) { - case false: - $this->_data[] = compact('context', 'trace') + $data; - return; - case 'log': - $this->log(compact('context', 'trace') + $data); - return; - } - - $data['trace'] = $trace; - $data['id'] = 'cakeErr' . uniqid(); - $tpl = array_merge($this->_templates['base'], $this->_templates[$this->_outputFormat]); - - if (isset($tpl['links'])) { - foreach ($tpl['links'] as $key => $val) { - $links[$key] = CakeText::insert($val, $data, $insertOpts); - } - } - - if (!empty($tpl['escapeContext'])) { - $context = h($context); - $data['description'] = h($data['description']); - } - - $infoData = compact('code', 'context', 'trace'); - foreach ($infoData as $key => $value) { - if (empty($value) || !isset($tpl[$key])) { - continue; - } - if (is_array($value)) { - $value = implode("\n", $value); - } - $info .= CakeText::insert($tpl[$key], array($key => $value) + $data, $insertOpts); - } - $links = implode(' ', $links); - - if (isset($tpl['callback']) && is_callable($tpl['callback'])) { - return call_user_func($tpl['callback'], $data, compact('links', 'info')); - } - echo CakeText::insert($tpl['error'], compact('links', 'info') + $data, $insertOpts); - } - -/** - * Get the type of the given variable. Will return the class name - * for objects. - * - * @param mixed $var The variable to get the type of - * @return string The type of variable. - */ - public static function getType($var) { - if (is_object($var)) { - return get_class($var); - } - if ($var === null) { - return 'null'; - } - if (is_string($var)) { - return 'string'; - } - if (is_array($var)) { - return 'array'; - } - if (is_int($var)) { - return 'integer'; - } - if (is_bool($var)) { - return 'boolean'; - } - if (is_float($var)) { - return 'float'; - } - if (is_resource($var)) { - return 'resource'; - } - return 'unknown'; - } - -/** - * Verifies that the application's salt and cipher seed value has been changed from the default value. - * - * @return void - */ - public static function checkSecurityKeys() { - if (Configure::read('Security.salt') === 'DYhG93b0qyJfIxfs2guVoUubWwvniR2G0FgaC9mi') { - trigger_error(__d('cake_dev', 'Please change the value of %s in %s to a salt value specific to your application.', '\'Security.salt\'', CONFIG . 'core.php'), E_USER_NOTICE); - } - - if (Configure::read('Security.cipherSeed') === '76859309657453542496749683645') { - trigger_error(__d('cake_dev', 'Please change the value of %s in %s to a numeric (digits only) seed value specific to your application.', '\'Security.cipherSeed\'', CONFIG . 'core.php'), E_USER_NOTICE); - } - } +class Debugger +{ + + /** + * A list of errors generated by the application. + * + * @var array + */ + public $errors = []; + + /** + * The current output format. + * + * @var string + */ + protected $_outputFormat = 'js'; + + /** + * Templates used when generating trace or error strings. Can be global or indexed by the format + * value used in $_outputFormat. + * + * @var string + */ + protected $_templates = [ + 'log' => [ + 'trace' => '{:reference} - {:path}, line {:line}', + 'error' => "{:error} ({:code}): {:description} in [{:file}, line {:line}]" + ], + 'js' => [ + 'error' => '', + 'info' => '', + 'trace' => '
{:trace}
', + 'code' => '', + 'context' => '', + 'links' => [], + 'escapeContext' => true, + ], + 'html' => [ + 'trace' => '
Trace 

{:trace}

', + 'context' => '
Context 

{:context}

', + 'escapeContext' => true, + ], + 'txt' => [ + 'error' => "{:error}: {:code} :: {:description} on line {:line} of {:path}\n{:info}", + 'code' => '', + 'info' => '' + ], + 'base' => [ + 'traceLine' => '{:reference} - {:path}, line {:line}', + 'trace' => "Trace:\n{:trace}\n", + 'context' => "Context:\n{:context}\n", + ] + ]; + + /** + * Holds current output data when outputFormat is false. + * + * @var string + */ + protected $_data = []; + + /** + * Constructor. + */ + public function __construct() + { + $docRef = ini_get('docref_root'); + + if (empty($docRef) && function_exists('ini_set')) { + ini_set('docref_root', 'http://php.net/'); + } + if (!defined('E_RECOVERABLE_ERROR')) { + define('E_RECOVERABLE_ERROR', 4096); + } + + $e = '
';
+        $e .= '{:error} ({:code}): {:description} ';
+        $e .= '[{:path}, line {:line}]';
+
+        $e .= '';
+        $e .= '
'; + $this->_templates['js']['error'] = $e; + + $t = ''; + $this->_templates['js']['info'] = $t; + + $links = []; + $link = 'Code'; + $links['code'] = $link; + + $link = 'Context'; + $links['context'] = $link; + + $this->_templates['js']['links'] = $links; + + $this->_templates['js']['context'] = '
_templates['js']['context'] .= 'style="display: none;">{:context}
'; + + $this->_templates['js']['code'] = '
_templates['js']['code'] .= 'style="display: none;">{:code}
'; + + $e = '
{:error} ({:code}) : {:description} ';
+        $e .= '[{:path}, line {:line}]
'; + $this->_templates['html']['error'] = $e; + + $this->_templates['html']['context'] = '
Context ';
+        $this->_templates['html']['context'] .= '

{:context}

'; + } + + /** + * Recursively formats and outputs the contents of the supplied variable. + * + * @param mixed $var the variable to dump + * @param int $depth The depth to output to. Defaults to 3. + * @return void + * @see Debugger::exportVar() + * @link https://book.cakephp.org/2.0/en/development/debugging.html#Debugger::dump + */ + public static function dump($var, $depth = 3) + { + pr(static::exportVar($var, $depth)); + } + + /** + * Converts a variable to a string for debug output. + * + * *Note:* The following keys will have their contents + * replaced with `*****`: + * + * - password + * - login + * - host + * - database + * - port + * + * This is done to protect database credentials, which could be accidentally + * shown in an error message if CakePHP is deployed in development mode. + * + * @param string $var Variable to convert + * @param int $depth The depth to output to. Defaults to 3. + * @return string Variable as a formatted string + * @link https://book.cakephp.org/2.0/en/development/debugging.html#Debugger::exportVar + */ + public static function exportVar($var, $depth = 3) + { + return static::_export($var, $depth, 0); + } + + /** + * Protected export function used to keep track of indentation and recursion. + * + * @param mixed $var The variable to dump. + * @param int $depth The remaining depth. + * @param int $indent The current indentation level. + * @return string The dumped variable. + */ + protected static function _export($var, $depth, $indent) + { + switch (static::getType($var)) { + case 'boolean': + return ($var) ? 'true' : 'false'; + case 'integer': + return '(int) ' . $var; + case 'float': + return '(float) ' . $var; + case 'string': + if (trim($var) === '') { + return "''"; + } + return "'" . $var . "'"; + case 'array': + return static::_array($var, $depth - 1, $indent + 1); + case 'resource': + return strtolower(gettype($var)); + case 'null': + return 'null'; + case 'unknown': + return 'unknown'; + default: + return static::_object($var, $depth - 1, $indent + 1); + } + } + + /** + * Get the type of the given variable. Will return the class name + * for objects. + * + * @param mixed $var The variable to get the type of + * @return string The type of variable. + */ + public static function getType($var) + { + if (is_object($var)) { + return get_class($var); + } + if ($var === null) { + return 'null'; + } + if (is_string($var)) { + return 'string'; + } + if (is_array($var)) { + return 'array'; + } + if (is_int($var)) { + return 'integer'; + } + if (is_bool($var)) { + return 'boolean'; + } + if (is_float($var)) { + return 'float'; + } + if (is_resource($var)) { + return 'resource'; + } + return 'unknown'; + } + + /** + * Export an array type object. Filters out keys used in datasource configuration. + * + * The following keys are replaced with ***'s + * + * - password + * - login + * - host + * - database + * - port + * + * @param array $var The array to export. + * @param int $depth The current depth, used for recursion tracking. + * @param int $indent The current indentation level. + * @return string Exported array. + */ + protected static function _array(array $var, $depth, $indent) + { + $secrets = [ + 'password' => '*****', + 'login' => '*****', + 'host' => '*****', + 'database' => '*****', + 'port' => '*****' + ]; + $replace = array_intersect_key($secrets, $var); + $var = $replace + $var; + + $out = "array("; + $break = $end = null; + if (!empty($var)) { + $break = "\n" . str_repeat("\t", $indent); + $end = "\n" . str_repeat("\t", $indent - 1); + } + $vars = []; + + if ($depth >= 0) { + foreach ($var as $key => $val) { + // Sniff for globals as !== explodes in < 5.4 + if ($key === 'GLOBALS' && is_array($val) && isset($val['GLOBALS'])) { + $val = '[recursion]'; + } else if ($val !== $var) { + $val = static::_export($val, $depth, $indent); + } + $vars[] = $break . static::exportVar($key) . + ' => ' . + $val; + } + } else { + $vars[] = $break . '[maximum depth reached]'; + } + return $out . implode(',', $vars) . $end . ')'; + } + + /** + * Handles object to string conversion. + * + * @param string $var Object to convert + * @param int $depth The current depth, used for tracking recursion. + * @param int $indent The current indentation level. + * @return string + * @see Debugger::exportVar() + */ + protected static function _object($var, $depth, $indent) + { + $out = ''; + $props = []; + + $className = get_class($var); + $out .= 'object(' . $className . ') {'; + + if ($depth > 0) { + $end = "\n" . str_repeat("\t", $indent - 1); + $break = "\n" . str_repeat("\t", $indent); + $objectVars = get_object_vars($var); + foreach ($objectVars as $key => $value) { + $value = static::_export($value, $depth - 1, $indent); + $props[] = "$key => " . $value; + } + + if (version_compare(PHP_VERSION, '5.3.0') >= 0) { + $ref = new ReflectionObject($var); + + $filters = [ + ReflectionProperty::IS_PROTECTED => 'protected', + ReflectionProperty::IS_PRIVATE => 'private', + ]; + foreach ($filters as $filter => $visibility) { + $reflectionProperties = $ref->getProperties($filter); + foreach ($reflectionProperties as $reflectionProperty) { + $reflectionProperty->setAccessible(true); + $property = $reflectionProperty->getValue($var); + + $value = static::_export($property, $depth - 1, $indent); + $key = $reflectionProperty->name; + $props[] = sprintf('[%s] %s => %s', $visibility, $key, $value); + } + } + } + + $out .= $break . implode($break, $props) . $end; + } + $out .= '}'; + return $out; + } + + /** + * Overrides PHP's default error handling. + * + * @param int $code Code of error + * @param string $description Error description + * @param string $file File on which error occurred + * @param int $line Line that triggered the error + * @param array $context Context + * @return bool|null True if error was handled, otherwise null. + * @deprecated 3.0.0 Will be removed in 3.0. This function is superseded by Debugger::outputError(). + */ + public static function showError($code, $description, $file = null, $line = null, $context = null) + { + $self = Debugger::getInstance(); + + if (empty($file)) { + $file = '[internal]'; + } + if (empty($line)) { + $line = '??'; + } + + $info = compact('code', 'description', 'file', 'line'); + if (!in_array($info, $self->errors)) { + $self->errors[] = $info; + } else { + return null; + } + + switch ($code) { + case E_PARSE: + case E_ERROR: + case E_CORE_ERROR: + case E_COMPILE_ERROR: + case E_USER_ERROR: + $error = 'Fatal Error'; + $level = LOG_ERR; + break; + case E_WARNING: + case E_USER_WARNING: + case E_COMPILE_WARNING: + case E_RECOVERABLE_ERROR: + $error = 'Warning'; + $level = LOG_WARNING; + break; + case E_NOTICE: + case E_USER_NOTICE: + $error = 'Notice'; + $level = LOG_NOTICE; + break; + case E_DEPRECATED: + case E_USER_DEPRECATED: + $error = 'Deprecated'; + $level = LOG_NOTICE; + break; + default: + return null; + } + + $data = compact( + 'level', 'error', 'code', 'description', 'file', 'line', 'context' + ); + echo $self->outputError($data); + + if ($error === 'Fatal Error') { + exit(); + } + return true; + } + + /** + * Takes a processed array of data from an error and displays it in the chosen format. + * + * @param string $data Data to output. + * @return void + */ + public function outputError($data) + { + $defaults = [ + 'level' => 0, + 'error' => 0, + 'code' => 0, + 'description' => '', + 'file' => '', + 'line' => 0, + 'context' => [], + 'start' => 2, + ]; + $data += $defaults; + + $files = $this->trace(['start' => $data['start'], 'format' => 'points']); + $code = ''; + $file = null; + if (isset($files[0]['file'])) { + $file = $files[0]; + } else if (isset($files[1]['file'])) { + $file = $files[1]; + } + if ($file) { + $code = $this->excerpt($file['file'], $file['line'] - 1, 1); + } + $trace = $this->trace(['start' => $data['start'], 'depth' => '20']); + $insertOpts = ['before' => '{:', 'after' => '}']; + $context = []; + $links = []; + $info = ''; + + foreach ((array)$data['context'] as $var => $value) { + $context[] = "\${$var} = " . $this->exportVar($value, 3); + } + + switch ($this->_outputFormat) { + case false: + $this->_data[] = compact('context', 'trace') + $data; + return; + case 'log': + $this->log(compact('context', 'trace') + $data); + return; + } + + $data['trace'] = $trace; + $data['id'] = 'cakeErr' . uniqid(); + $tpl = array_merge($this->_templates['base'], $this->_templates[$this->_outputFormat]); + + if (isset($tpl['links'])) { + foreach ($tpl['links'] as $key => $val) { + $links[$key] = CakeText::insert($val, $data, $insertOpts); + } + } + + if (!empty($tpl['escapeContext'])) { + $context = h($context); + $data['description'] = h($data['description']); + } + + $infoData = compact('code', 'context', 'trace'); + foreach ($infoData as $key => $value) { + if (empty($value) || !isset($tpl[$key])) { + continue; + } + if (is_array($value)) { + $value = implode("\n", $value); + } + $info .= CakeText::insert($tpl[$key], [$key => $value] + $data, $insertOpts); + } + $links = implode(' ', $links); + + if (isset($tpl['callback']) && is_callable($tpl['callback'])) { + return call_user_func($tpl['callback'], $data, compact('links', 'info')); + } + echo CakeText::insert($tpl['error'], compact('links', 'info') + $data, $insertOpts); + } + + /** + * Grabs an excerpt from a file and highlights a given line of code. + * + * Usage: + * + * `Debugger::excerpt('/path/to/file', 100, 4);` + * + * The above would return an array of 8 items. The 4th item would be the provided line, + * and would be wrapped in ``. All of the lines + * are processed with highlight_string() as well, so they have basic PHP syntax highlighting + * applied. + * + * @param string $file Absolute path to a PHP file + * @param int $line Line number to highlight + * @param int $context Number of lines of context to extract above and below $line + * @return array Set of lines highlighted + * @see http://php.net/highlight_string + * @link https://book.cakephp.org/2.0/en/development/debugging.html#Debugger::excerpt + */ + public static function excerpt($file, $line, $context = 2) + { + $lines = []; + if (!file_exists($file)) { + return []; + } + $data = file_get_contents($file); + if (empty($data)) { + return $lines; + } + if (strpos($data, "\n") !== false) { + $data = explode("\n", $data); + } + if (!isset($data[$line])) { + return $lines; + } + for ($i = $line - ($context + 1); $i < $line + $context; $i++) { + if (!isset($data[$i])) { + continue; + } + $string = str_replace(["\r\n", "\n"], "", static::_highlight($data[$i])); + if ($i == $line) { + $lines[] = '' . $string . ''; + } else { + $lines[] = $string; + } + } + return $lines; + } + + /** + * Wraps the highlight_string function in case the server API does not + * implement the function as it is the case of the HipHop interpreter + * + * @param string $str the string to convert + * @return string + */ + protected static function _highlight($str) + { + if (function_exists('hphp_log') || function_exists('hphp_gettid')) { + return htmlentities($str); + } + $added = false; + if (strpos($str, '', + '', + $highlight + ); + } + return $highlight; + } + + /** + * Creates an entry in the log file. The log entry will contain a stack trace from where it was called. + * as well as export the variable using exportVar. By default the log is written to the debug log. + * + * @param mixed $var Variable or content to log + * @param int|string $level Type of log to use. Defaults to LOG_DEBUG. When value is an integer + * or a string matching the recognized levels, then it will + * be treated as a log level. Otherwise it's treated as a scope. + * @param int $depth The depth to output to. Defaults to 3. + * @return void + * @link https://book.cakephp.org/2.0/en/development/debugging.html#Debugger::log + */ + public static function log($var, $level = LOG_DEBUG, $depth = 3) + { + $source = static::trace(['start' => 1]) . "\n"; + CakeLog::write($level, "\n" . $source . static::exportVar($var, $depth)); + } + + /** + * Outputs a stack trace based on the supplied options. + * + * ### Options + * + * - `depth` - The number of stack frames to return. Defaults to 999 + * - `format` - The format you want the return. Defaults to the currently selected format. If + * format is 'array' or 'points' the return will be an array. + * - `args` - Should arguments for functions be shown? If true, the arguments for each method call + * will be displayed. + * - `start` - The stack frame to start generating a trace from. Defaults to 0 + * + * @param array $options Format for outputting stack trace + * @return mixed Formatted stack trace + * @link https://book.cakephp.org/2.0/en/development/debugging.html#Debugger::trace + */ + public static function trace($options = []) + { + $self = Debugger::getInstance(); + $defaults = [ + 'depth' => 999, + 'format' => $self->_outputFormat, + 'args' => false, + 'start' => 0, + 'scope' => null, + 'exclude' => ['call_user_func_array', 'trigger_error'] + ]; + $options = Hash::merge($defaults, $options); + + $backtrace = debug_backtrace(); + $count = count($backtrace); + $back = []; + + $_trace = [ + 'line' => '??', + 'file' => '[internal]', + 'class' => null, + 'function' => '[main]' + ]; + + for ($i = $options['start']; $i < $count && $i < $options['depth']; $i++) { + $trace = array_merge(['file' => '[internal]', 'line' => '??'], $backtrace[$i]); + $signature = $reference = '[main]'; + + if (isset($backtrace[$i + 1])) { + $next = array_merge($_trace, $backtrace[$i + 1]); + $signature = $reference = $next['function']; + + if (!empty($next['class'])) { + $signature = $next['class'] . '::' . $next['function']; + $reference = $signature . '('; + if ($options['args'] && isset($next['args'])) { + $args = []; + foreach ($next['args'] as $arg) { + $args[] = Debugger::exportVar($arg); + } + $reference .= implode(', ', $args); + } + $reference .= ')'; + } + } + if (in_array($signature, $options['exclude'])) { + continue; + } + if ($options['format'] === 'points' && $trace['file'] !== '[internal]') { + $back[] = ['file' => $trace['file'], 'line' => $trace['line']]; + } else if ($options['format'] === 'array') { + $back[] = $trace; + } else { + if (isset($self->_templates[$options['format']]['traceLine'])) { + $tpl = $self->_templates[$options['format']]['traceLine']; + } else { + $tpl = $self->_templates['base']['traceLine']; + } + $trace['path'] = static::trimPath($trace['file']); + $trace['reference'] = $reference; + unset($trace['object'], $trace['args']); + $back[] = CakeText::insert($tpl, $trace, ['before' => '{:', 'after' => '}']); + } + } + + if ($options['format'] === 'array' || $options['format'] === 'points') { + return $back; + } + return implode("\n", $back); + } + + /** + * Returns a reference to the Debugger singleton object instance. + * + * @param string $class Debugger class name. + * @return object + */ + public static function getInstance($class = null) + { + static $instance = []; + if (!empty($class)) { + if (!$instance || strtolower($class) != strtolower(get_class($instance[0]))) { + $instance[0] = new $class(); + } + } + if (!$instance) { + $instance[0] = new Debugger(); + } + return $instance[0]; + } + + /** + * Shortens file paths by replacing the application base path with 'APP', and the CakePHP core + * path with 'CORE'. + * + * @param string $path Path to shorten + * @return string Normalized path + */ + public static function trimPath($path) + { + if (!defined('CAKE_CORE_INCLUDE_PATH') || !defined('APP')) { + return $path; + } + + if (strpos($path, APP) === 0) { + return str_replace(APP, 'APP' . DS, $path); + } else if (strpos($path, CAKE_CORE_INCLUDE_PATH) === 0) { + return str_replace(CAKE_CORE_INCLUDE_PATH, 'CORE', $path); + } else if (strpos($path, ROOT) === 0) { + return str_replace(ROOT, 'ROOT', $path); + } + + return $path; + } + + /** + * Switches output format, updates format strings. + * Can be used to switch the active output format: + * + * @param string $format Format to use, including 'js' for JavaScript-enhanced HTML, 'html' for + * straight HTML output, or 'txt' for unformatted text. + * @param array $strings Template strings to be used for the output format. + * @return string + * @deprecated 3.0.0 Use Debugger::outputAs() and Debugger::addFormat(). Will be removed + * in 3.0 + */ + public static function output($format = null, $strings = []) + { + $self = Debugger::getInstance(); + $data = null; + + if ($format === null) { + return Debugger::outputAs(); + } + + if (!empty($strings)) { + return Debugger::addFormat($format, $strings); + } + + if ($format === true && !empty($self->_data)) { + $data = $self->_data; + $self->_data = []; + $format = false; + } + Debugger::outputAs($format); + return $data; + } + + /** + * Get/Set the output format for Debugger error rendering. + * + * @param string $format The format you want errors to be output as. + * Leave null to get the current format. + * @return mixed Returns null when setting. Returns the current format when getting. + * @throws CakeException when choosing a format that doesn't exist. + */ + public static function outputAs($format = null) + { + $self = Debugger::getInstance(); + if ($format === null) { + return $self->_outputFormat; + } + if ($format !== false && !isset($self->_templates[$format])) { + throw new CakeException(__d('cake_dev', 'Invalid Debugger output format.')); + } + $self->_outputFormat = $format; + } + + /** + * Add an output format or update a format in Debugger. + * + * `Debugger::addFormat('custom', $data);` + * + * Where $data is an array of strings that use CakeText::insert() variable + * replacement. The template vars should be in a `{:id}` style. + * An error formatter can have the following keys: + * + * - 'error' - Used for the container for the error message. Gets the following template + * variables: `id`, `error`, `code`, `description`, `path`, `line`, `links`, `info` + * - 'info' - A combination of `code`, `context` and `trace`. Will be set with + * the contents of the other template keys. + * - 'trace' - The container for a stack trace. Gets the following template + * variables: `trace` + * - 'context' - The container element for the context variables. + * Gets the following templates: `id`, `context` + * - 'links' - An array of HTML links that are used for creating links to other resources. + * Typically this is used to create javascript links to open other sections. + * Link keys, are: `code`, `context`, `help`. See the js output format for an + * example. + * - 'traceLine' - Used for creating lines in the stacktrace. Gets the following + * template variables: `reference`, `path`, `line` + * + * Alternatively if you want to use a custom callback to do all the formatting, you can use + * the callback key, and provide a callable: + * + * `Debugger::addFormat('custom', array('callback' => array($foo, 'outputError'));` + * + * The callback can expect two parameters. The first is an array of all + * the error data. The second contains the formatted strings generated using + * the other template strings. Keys like `info`, `links`, `code`, `context` and `trace` + * will be present depending on the other templates in the format type. + * + * @param string $format Format to use, including 'js' for JavaScript-enhanced HTML, 'html' for + * straight HTML output, or 'txt' for unformatted text. + * @param array $strings Template strings, or a callback to be used for the output format. + * @return The resulting format string set. + */ + public static function addFormat($format, array $strings) + { + $self = Debugger::getInstance(); + if (isset($self->_templates[$format])) { + if (isset($strings['links'])) { + $self->_templates[$format]['links'] = array_merge( + $self->_templates[$format]['links'], + $strings['links'] + ); + unset($strings['links']); + } + $self->_templates[$format] = array_merge($self->_templates[$format], $strings); + } else { + $self->_templates[$format] = $strings; + } + return $self->_templates[$format]; + } + + /** + * Verifies that the application's salt and cipher seed value has been changed from the default value. + * + * @return void + */ + public static function checkSecurityKeys() + { + if (Configure::read('Security.salt') === 'DYhG93b0qyJfIxfs2guVoUubWwvniR2G0FgaC9mi') { + trigger_error(__d('cake_dev', 'Please change the value of %s in %s to a salt value specific to your application.', '\'Security.salt\'', CONFIG . 'core.php'), E_USER_NOTICE); + } + + if (Configure::read('Security.cipherSeed') === '76859309657453542496749683645') { + trigger_error(__d('cake_dev', 'Please change the value of %s in %s to a numeric (digits only) seed value specific to your application.', '\'Security.cipherSeed\'', CONFIG . 'core.php'), E_USER_NOTICE); + } + } } diff --git a/lib/Cake/Utility/File.php b/lib/Cake/Utility/File.php index 6c4bed10..ea506840 100755 --- a/lib/Cake/Utility/File.php +++ b/lib/Cake/Utility/File.php @@ -23,596 +23,629 @@ * * @package Cake.Utility */ -class File { - -/** - * Folder object of the file - * - * @var Folder - * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#File::$Folder - */ - public $Folder = null; - -/** - * File name - * - * @var string - * https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#File::$name - */ - public $name = null; - -/** - * File info - * - * @var array - * https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#File::$info - */ - public $info = array(); - -/** - * Holds the file handler resource if the file is opened - * - * @var resource - * https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#File::$handle - */ - public $handle = null; - -/** - * Enable locking for file reading and writing - * - * @var bool - * https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#File::$lock - */ - public $lock = null; - -/** - * Path property - * - * Current file's absolute path - * - * @var mixed - * https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#File::$path - */ - public $path = null; - -/** - * Constructor - * - * @param string $path Path to file - * @param bool $create Create file if it does not exist (if true) - * @param int $mode Mode to apply to the folder holding the file - * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#File - */ - public function __construct($path, $create = false, $mode = 0755) { - $this->Folder = new Folder(dirname($path), $create, $mode); - if (!is_dir($path)) { - $this->name = basename($path); - } - $this->pwd(); - $create && !$this->exists() && $this->safe($path) && $this->create(); - } - -/** - * Closes the current file if it is opened - */ - public function __destruct() { - $this->close(); - } - -/** - * Creates the file. - * - * @return bool Success - * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#File::create - */ - public function create() { - $dir = $this->Folder->pwd(); - if (is_dir($dir) && is_writable($dir) && !$this->exists()) { - if (touch($this->path)) { - return true; - } - } - return false; - } - -/** - * Opens the current file with a given $mode - * - * @param string $mode A valid 'fopen' mode string (r|w|a ...) - * @param bool $force If true then the file will be re-opened even if its already opened, otherwise it won't - * @return bool True on success, false on failure - * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#File::open - */ - public function open($mode = 'r', $force = false) { - if (!$force && is_resource($this->handle)) { - return true; - } - if ($this->exists() === false) { - if ($this->create() === false) { - return false; - } - } - - $this->handle = fopen($this->path, $mode); - if (is_resource($this->handle)) { - return true; - } - return false; - } - -/** - * Return the contents of this file as a string. - * - * @param string $bytes where to start - * @param string $mode A `fread` compatible mode. - * @param bool $force If true then the file will be re-opened even if its already opened, otherwise it won't - * @return mixed string on success, false on failure - * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#File::read - */ - public function read($bytes = false, $mode = 'rb', $force = false) { - if ($bytes === false && $this->lock === null) { - return file_get_contents($this->path); - } - if ($this->open($mode, $force) === false) { - return false; - } - if ($this->lock !== null && flock($this->handle, LOCK_SH) === false) { - return false; - } - if (is_int($bytes)) { - return fread($this->handle, $bytes); - } - - $data = ''; - while (!feof($this->handle)) { - $data .= fgets($this->handle, 4096); - } - - if ($this->lock !== null) { - flock($this->handle, LOCK_UN); - } - if ($bytes === false) { - $this->close(); - } - return trim($data); - } - -/** - * Sets or gets the offset for the currently opened file. - * - * @param int|bool $offset The $offset in bytes to seek. If set to false then the current offset is returned. - * @param int $seek PHP Constant SEEK_SET | SEEK_CUR | SEEK_END determining what the $offset is relative to - * @return mixed True on success, false on failure (set mode), false on failure or integer offset on success (get mode) - * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#File::offset - */ - public function offset($offset = false, $seek = SEEK_SET) { - if ($offset === false) { - if (is_resource($this->handle)) { - return ftell($this->handle); - } - } elseif ($this->open() === true) { - return fseek($this->handle, $offset, $seek) === 0; - } - return false; - } - -/** - * Prepares an ASCII string for writing. Converts line endings to the - * correct terminator for the current platform. If Windows, "\r\n" will be used, - * all other platforms will use "\n" - * - * @param string $data Data to prepare for writing. - * @param bool $forceWindows If true forces usage Windows newline string. - * @return string The with converted line endings. - * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#File::prepare - */ - public static function prepare($data, $forceWindows = false) { - $lineBreak = "\n"; - if (DIRECTORY_SEPARATOR === '\\' || $forceWindows === true) { - $lineBreak = "\r\n"; - } - return strtr($data, array("\r\n" => $lineBreak, "\n" => $lineBreak, "\r" => $lineBreak)); - } - -/** - * Write given data to this file. - * - * @param string $data Data to write to this File. - * @param string $mode Mode of writing. {@link http://php.net/fwrite See fwrite()}. - * @param bool $force Force the file to open - * @return bool Success - * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#File::write - */ - public function write($data, $mode = 'w', $force = false) { - $success = false; - if ($this->open($mode, $force) === true) { - if ($this->lock !== null) { - if (flock($this->handle, LOCK_EX) === false) { - return false; - } - } - - if (fwrite($this->handle, $data) !== false) { - $success = true; - } - if ($this->lock !== null) { - flock($this->handle, LOCK_UN); - } - } - return $success; - } - -/** - * Append given data string to this file. - * - * @param string $data Data to write - * @param string $force Force the file to open - * @return bool Success - * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#File::append - */ - public function append($data, $force = false) { - return $this->write($data, 'a', $force); - } - -/** - * Closes the current file if it is opened. - * - * @return bool True if closing was successful or file was already closed, otherwise false - * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#File::close - */ - public function close() { - if (!is_resource($this->handle)) { - return true; - } - return fclose($this->handle); - } - -/** - * Deletes the file. - * - * @return bool Success - * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#File::delete - */ - public function delete() { - if (is_resource($this->handle)) { - fclose($this->handle); - $this->handle = null; - } - if ($this->exists()) { - return unlink($this->path); - } - return false; - } - -/** - * Returns the file info as an array with the following keys: - * - * - dirname - * - basename - * - extension - * - filename - * - filesize - * - mime - * - * @return array File information. - * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#File::info - */ - public function info() { - if (!$this->info) { - $this->info = pathinfo($this->path); - } - if (!isset($this->info['filename'])) { - $this->info['filename'] = $this->name(); - } - if (!isset($this->info['filesize'])) { - $this->info['filesize'] = $this->size(); - } - if (!isset($this->info['mime'])) { - $this->info['mime'] = $this->mime(); - } - return $this->info; - } - -/** - * Returns the file extension. - * - * @return string The file extension - * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#File::ext - */ - public function ext() { - if (!$this->info) { - $this->info(); - } - if (isset($this->info['extension'])) { - return $this->info['extension']; - } - return false; - } - -/** - * Returns the file name without extension. - * - * @return string The file name without extension. - * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#File::name - */ - public function name() { - if (!$this->info) { - $this->info(); - } - if (isset($this->info['extension'])) { - return basename($this->name, '.' . $this->info['extension']); - } elseif ($this->name) { - return $this->name; - } - return false; - } - -/** - * Makes file name safe for saving - * - * @param string $name The name of the file to make safe if different from $this->name - * @param string $ext The name of the extension to make safe if different from $this->ext - * @return string ext The extension of the file - * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#File::safe - */ - public function safe($name = null, $ext = null) { - if (!$name) { - $name = $this->name; - } - if (!$ext) { - $ext = $this->ext(); - } - return preg_replace("/(?:[^\w\.-]+)/", "_", basename($name, $ext)); - } - -/** - * Get md5 Checksum of file with previous check of Filesize - * - * @param int|bool $maxsize in MB or true to force - * @return string|false md5 Checksum {@link http://php.net/md5_file See md5_file()}, or false in case of an error - * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#File::md5 - */ - public function md5($maxsize = 5) { - if ($maxsize === true) { - return md5_file($this->path); - } - - $size = $this->size(); - if ($size && $size < ($maxsize * 1024) * 1024) { - return md5_file($this->path); - } - - return false; - } - -/** - * Returns the full path of the file. - * - * @return string Full path to the file - * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#File::pwd - */ - public function pwd() { - if ($this->path === null) { - $dir = $this->Folder->pwd(); - if (is_dir($dir)) { - $this->path = $this->Folder->slashTerm($dir) . $this->name; - } - } - return $this->path; - } - -/** - * Returns true if the file exists. - * - * @return bool True if it exists, false otherwise - * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#File::exists - */ - public function exists() { - $this->clearStatCache(); - return (file_exists($this->path) && is_file($this->path)); - } - -/** - * Returns the "chmod" (permissions) of the file. - * - * @return string|false Permissions for the file, or false in case of an error - * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#File::perms - */ - public function perms() { - if ($this->exists()) { - return substr(sprintf('%o', fileperms($this->path)), -4); - } - return false; - } - -/** - * Returns the file size - * - * @return int|false Size of the file in bytes, or false in case of an error - * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#File::size - */ - public function size() { - if ($this->exists()) { - return filesize($this->path); - } - return false; - } - -/** - * Returns true if the file is writable. - * - * @return bool True if it's writable, false otherwise - * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#File::writable - */ - public function writable() { - return is_writable($this->path); - } - -/** - * Returns true if the File is executable. - * - * @return bool True if it's executable, false otherwise - * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#File::executable - */ - public function executable() { - return is_executable($this->path); - } - -/** - * Returns true if the file is readable. - * - * @return bool True if file is readable, false otherwise - * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#File::readable - */ - public function readable() { - return is_readable($this->path); - } - -/** - * Returns the file's owner. - * - * @return int|false The file owner, or false in case of an error - * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#File::owner - */ - public function owner() { - if ($this->exists()) { - return fileowner($this->path); - } - return false; - } - -/** - * Returns the file's group. - * - * @return int|false The file group, or false in case of an error - * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#File::group - */ - public function group() { - if ($this->exists()) { - return filegroup($this->path); - } - return false; - } - -/** - * Returns last access time. - * - * @return int|false Timestamp of last access time, or false in case of an error - * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#File::lastAccess - */ - public function lastAccess() { - if ($this->exists()) { - return fileatime($this->path); - } - return false; - } - -/** - * Returns last modified time. - * - * @return int|false Timestamp of last modification, or false in case of an error - * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#File::lastChange - */ - public function lastChange() { - if ($this->exists()) { - return filemtime($this->path); - } - return false; - } - -/** - * Returns the current folder. - * - * @return Folder Current folder - * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#File::Folder - */ - public function folder() { - return $this->Folder; - } - -/** - * Copy the File to $dest - * - * @param string $dest Destination for the copy - * @param bool $overwrite Overwrite $dest if exists - * @return bool Success - * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#File::copy - */ - public function copy($dest, $overwrite = true) { - if (!$this->exists() || is_file($dest) && !$overwrite) { - return false; - } - return copy($this->path, $dest); - } - -/** - * Get the mime type of the file. Uses the finfo extension if - * its available, otherwise falls back to mime_content_type - * - * @return false|string The mimetype of the file, or false if reading fails. - */ - public function mime() { - if (!$this->exists()) { - return false; - } - if (function_exists('finfo_open')) { - $finfo = finfo_open(FILEINFO_MIME); - $finfo = finfo_file($finfo, $this->pwd()); - if (!$finfo) { - return false; - } - list($type) = explode(';', $finfo); - return $type; - } - if (function_exists('mime_content_type')) { - return mime_content_type($this->pwd()); - } - return false; - } - -/** - * Clear PHP's internal stat cache - * - * For 5.3 onwards it's possible to clear cache for just a single file. Passing true - * will clear all the stat cache. - * - * @param bool $all Clear all cache or not - * @return void - */ - public function clearStatCache($all = false) { - if ($all === false && version_compare(PHP_VERSION, '5.3.0') >= 0) { - return clearstatcache(true, $this->path); - } - - return clearstatcache(); - } - -/** - * Searches for a given text and replaces the text if found. - * - * @param string|array $search Text(s) to search for. - * @param string|array $replace Text(s) to replace with. - * @return bool Success - */ - public function replaceText($search, $replace) { - if (!$this->open('r+')) { - return false; - } - - if ($this->lock !== null) { - if (flock($this->handle, LOCK_EX) === false) { - return false; - } - } - - $replaced = $this->write(str_replace($search, $replace, $this->read()), 'w', true); - - if ($this->lock !== null) { - flock($this->handle, LOCK_UN); - } - $this->close(); - - return $replaced; - } +class File +{ + + /** + * Folder object of the file + * + * @var Folder + * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#File::$Folder + */ + public $Folder = null; + + /** + * File name + * + * @var string + * https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#File::$name + */ + public $name = null; + + /** + * File info + * + * @var array + * https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#File::$info + */ + public $info = []; + + /** + * Holds the file handler resource if the file is opened + * + * @var resource + * https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#File::$handle + */ + public $handle = null; + + /** + * Enable locking for file reading and writing + * + * @var bool + * https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#File::$lock + */ + public $lock = null; + + /** + * Path property + * + * Current file's absolute path + * + * @var mixed + * https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#File::$path + */ + public $path = null; + + /** + * Constructor + * + * @param string $path Path to file + * @param bool $create Create file if it does not exist (if true) + * @param int $mode Mode to apply to the folder holding the file + * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#File + */ + public function __construct($path, $create = false, $mode = 0755) + { + $this->Folder = new Folder(dirname($path), $create, $mode); + if (!is_dir($path)) { + $this->name = basename($path); + } + $this->pwd(); + $create && !$this->exists() && $this->safe($path) && $this->create(); + } + + /** + * Returns the full path of the file. + * + * @return string Full path to the file + * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#File::pwd + */ + public function pwd() + { + if ($this->path === null) { + $dir = $this->Folder->pwd(); + if (is_dir($dir)) { + $this->path = $this->Folder->slashTerm($dir) . $this->name; + } + } + return $this->path; + } + + /** + * Returns true if the file exists. + * + * @return bool True if it exists, false otherwise + * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#File::exists + */ + public function exists() + { + $this->clearStatCache(); + return (file_exists($this->path) && is_file($this->path)); + } + + /** + * Clear PHP's internal stat cache + * + * For 5.3 onwards it's possible to clear cache for just a single file. Passing true + * will clear all the stat cache. + * + * @param bool $all Clear all cache or not + * @return void + */ + public function clearStatCache($all = false) + { + if ($all === false && version_compare(PHP_VERSION, '5.3.0') >= 0) { + return clearstatcache(true, $this->path); + } + + return clearstatcache(); + } + + /** + * Makes file name safe for saving + * + * @param string $name The name of the file to make safe if different from $this->name + * @param string $ext The name of the extension to make safe if different from $this->ext + * @return string ext The extension of the file + * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#File::safe + */ + public function safe($name = null, $ext = null) + { + if (!$name) { + $name = $this->name; + } + if (!$ext) { + $ext = $this->ext(); + } + return preg_replace("/(?:[^\w\.-]+)/", "_", basename($name, $ext)); + } + + /** + * Returns the file extension. + * + * @return string The file extension + * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#File::ext + */ + public function ext() + { + if (!$this->info) { + $this->info(); + } + if (isset($this->info['extension'])) { + return $this->info['extension']; + } + return false; + } + + /** + * Returns the file info as an array with the following keys: + * + * - dirname + * - basename + * - extension + * - filename + * - filesize + * - mime + * + * @return array File information. + * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#File::info + */ + public function info() + { + if (!$this->info) { + $this->info = pathinfo($this->path); + } + if (!isset($this->info['filename'])) { + $this->info['filename'] = $this->name(); + } + if (!isset($this->info['filesize'])) { + $this->info['filesize'] = $this->size(); + } + if (!isset($this->info['mime'])) { + $this->info['mime'] = $this->mime(); + } + return $this->info; + } + + /** + * Returns the file name without extension. + * + * @return string The file name without extension. + * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#File::name + */ + public function name() + { + if (!$this->info) { + $this->info(); + } + if (isset($this->info['extension'])) { + return basename($this->name, '.' . $this->info['extension']); + } else if ($this->name) { + return $this->name; + } + return false; + } + + /** + * Returns the file size + * + * @return int|false Size of the file in bytes, or false in case of an error + * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#File::size + */ + public function size() + { + if ($this->exists()) { + return filesize($this->path); + } + return false; + } + + /** + * Get the mime type of the file. Uses the finfo extension if + * its available, otherwise falls back to mime_content_type + * + * @return false|string The mimetype of the file, or false if reading fails. + */ + public function mime() + { + if (!$this->exists()) { + return false; + } + if (function_exists('finfo_open')) { + $finfo = finfo_open(FILEINFO_MIME); + $finfo = finfo_file($finfo, $this->pwd()); + if (!$finfo) { + return false; + } + list($type) = explode(';', $finfo); + return $type; + } + if (function_exists('mime_content_type')) { + return mime_content_type($this->pwd()); + } + return false; + } + + /** + * Creates the file. + * + * @return bool Success + * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#File::create + */ + public function create() + { + $dir = $this->Folder->pwd(); + if (is_dir($dir) && is_writable($dir) && !$this->exists()) { + if (touch($this->path)) { + return true; + } + } + return false; + } + + /** + * Prepares an ASCII string for writing. Converts line endings to the + * correct terminator for the current platform. If Windows, "\r\n" will be used, + * all other platforms will use "\n" + * + * @param string $data Data to prepare for writing. + * @param bool $forceWindows If true forces usage Windows newline string. + * @return string The with converted line endings. + * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#File::prepare + */ + public static function prepare($data, $forceWindows = false) + { + $lineBreak = "\n"; + if (DIRECTORY_SEPARATOR === '\\' || $forceWindows === true) { + $lineBreak = "\r\n"; + } + return strtr($data, ["\r\n" => $lineBreak, "\n" => $lineBreak, "\r" => $lineBreak]); + } + + /** + * Closes the current file if it is opened + */ + public function __destruct() + { + $this->close(); + } + + /** + * Closes the current file if it is opened. + * + * @return bool True if closing was successful or file was already closed, otherwise false + * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#File::close + */ + public function close() + { + if (!is_resource($this->handle)) { + return true; + } + return fclose($this->handle); + } + + /** + * Sets or gets the offset for the currently opened file. + * + * @param int|bool $offset The $offset in bytes to seek. If set to false then the current offset is returned. + * @param int $seek PHP Constant SEEK_SET | SEEK_CUR | SEEK_END determining what the $offset is relative to + * @return mixed True on success, false on failure (set mode), false on failure or integer offset on success (get mode) + * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#File::offset + */ + public function offset($offset = false, $seek = SEEK_SET) + { + if ($offset === false) { + if (is_resource($this->handle)) { + return ftell($this->handle); + } + } else if ($this->open() === true) { + return fseek($this->handle, $offset, $seek) === 0; + } + return false; + } + + /** + * Opens the current file with a given $mode + * + * @param string $mode A valid 'fopen' mode string (r|w|a ...) + * @param bool $force If true then the file will be re-opened even if its already opened, otherwise it won't + * @return bool True on success, false on failure + * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#File::open + */ + public function open($mode = 'r', $force = false) + { + if (!$force && is_resource($this->handle)) { + return true; + } + if ($this->exists() === false) { + if ($this->create() === false) { + return false; + } + } + + $this->handle = fopen($this->path, $mode); + if (is_resource($this->handle)) { + return true; + } + return false; + } + + /** + * Append given data string to this file. + * + * @param string $data Data to write + * @param string $force Force the file to open + * @return bool Success + * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#File::append + */ + public function append($data, $force = false) + { + return $this->write($data, 'a', $force); + } + + /** + * Write given data to this file. + * + * @param string $data Data to write to this File. + * @param string $mode Mode of writing. {@link http://php.net/fwrite See fwrite()}. + * @param bool $force Force the file to open + * @return bool Success + * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#File::write + */ + public function write($data, $mode = 'w', $force = false) + { + $success = false; + if ($this->open($mode, $force) === true) { + if ($this->lock !== null) { + if (flock($this->handle, LOCK_EX) === false) { + return false; + } + } + + if (fwrite($this->handle, $data) !== false) { + $success = true; + } + if ($this->lock !== null) { + flock($this->handle, LOCK_UN); + } + } + return $success; + } + + /** + * Deletes the file. + * + * @return bool Success + * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#File::delete + */ + public function delete() + { + if (is_resource($this->handle)) { + fclose($this->handle); + $this->handle = null; + } + if ($this->exists()) { + return unlink($this->path); + } + return false; + } + + /** + * Get md5 Checksum of file with previous check of Filesize + * + * @param int|bool $maxsize in MB or true to force + * @return string|false md5 Checksum {@link http://php.net/md5_file See md5_file()}, or false in case of an error + * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#File::md5 + */ + public function md5($maxsize = 5) + { + if ($maxsize === true) { + return md5_file($this->path); + } + + $size = $this->size(); + if ($size && $size < ($maxsize * 1024) * 1024) { + return md5_file($this->path); + } + + return false; + } + + /** + * Returns the "chmod" (permissions) of the file. + * + * @return string|false Permissions for the file, or false in case of an error + * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#File::perms + */ + public function perms() + { + if ($this->exists()) { + return substr(sprintf('%o', fileperms($this->path)), -4); + } + return false; + } + + /** + * Returns true if the file is writable. + * + * @return bool True if it's writable, false otherwise + * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#File::writable + */ + public function writable() + { + return is_writable($this->path); + } + + /** + * Returns true if the File is executable. + * + * @return bool True if it's executable, false otherwise + * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#File::executable + */ + public function executable() + { + return is_executable($this->path); + } + + /** + * Returns true if the file is readable. + * + * @return bool True if file is readable, false otherwise + * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#File::readable + */ + public function readable() + { + return is_readable($this->path); + } + + /** + * Returns the file's owner. + * + * @return int|false The file owner, or false in case of an error + * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#File::owner + */ + public function owner() + { + if ($this->exists()) { + return fileowner($this->path); + } + return false; + } + + /** + * Returns the file's group. + * + * @return int|false The file group, or false in case of an error + * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#File::group + */ + public function group() + { + if ($this->exists()) { + return filegroup($this->path); + } + return false; + } + + /** + * Returns last access time. + * + * @return int|false Timestamp of last access time, or false in case of an error + * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#File::lastAccess + */ + public function lastAccess() + { + if ($this->exists()) { + return fileatime($this->path); + } + return false; + } + + /** + * Returns last modified time. + * + * @return int|false Timestamp of last modification, or false in case of an error + * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#File::lastChange + */ + public function lastChange() + { + if ($this->exists()) { + return filemtime($this->path); + } + return false; + } + + /** + * Returns the current folder. + * + * @return Folder Current folder + * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#File::Folder + */ + public function folder() + { + return $this->Folder; + } + + /** + * Copy the File to $dest + * + * @param string $dest Destination for the copy + * @param bool $overwrite Overwrite $dest if exists + * @return bool Success + * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#File::copy + */ + public function copy($dest, $overwrite = true) + { + if (!$this->exists() || is_file($dest) && !$overwrite) { + return false; + } + return copy($this->path, $dest); + } + + /** + * Searches for a given text and replaces the text if found. + * + * @param string|array $search Text(s) to search for. + * @param string|array $replace Text(s) to replace with. + * @return bool Success + */ + public function replaceText($search, $replace) + { + if (!$this->open('r+')) { + return false; + } + + if ($this->lock !== null) { + if (flock($this->handle, LOCK_EX) === false) { + return false; + } + } + + $replaced = $this->write(str_replace($search, $replace, $this->read()), 'w', true); + + if ($this->lock !== null) { + flock($this->handle, LOCK_UN); + } + $this->close(); + + return $replaced; + } + + /** + * Return the contents of this file as a string. + * + * @param string $bytes where to start + * @param string $mode A `fread` compatible mode. + * @param bool $force If true then the file will be re-opened even if its already opened, otherwise it won't + * @return mixed string on success, false on failure + * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#File::read + */ + public function read($bytes = false, $mode = 'rb', $force = false) + { + if ($bytes === false && $this->lock === null) { + return file_get_contents($this->path); + } + if ($this->open($mode, $force) === false) { + return false; + } + if ($this->lock !== null && flock($this->handle, LOCK_SH) === false) { + return false; + } + if (is_int($bytes)) { + return fread($this->handle, $bytes); + } + + $data = ''; + while (!feof($this->handle)) { + $data .= fgets($this->handle, 4096); + } + + if ($this->lock !== null) { + flock($this->handle, LOCK_UN); + } + if ($bytes === false) { + $this->close(); + } + return trim($data); + } } diff --git a/lib/Cake/Utility/Folder.php b/lib/Cake/Utility/Folder.php index 7decebe6..9706e4f8 100755 --- a/lib/Cake/Utility/Folder.php +++ b/lib/Cake/Utility/Folder.php @@ -20,885 +20,913 @@ * * @package Cake.Utility */ -class Folder { - -/** - * Default scheme for Folder::copy - * Recursively merges subfolders with the same name - * - * @var string - */ - const MERGE = 'merge'; - -/** - * Overwrite scheme for Folder::copy - * subfolders with the same name will be replaced - * - * @var string - */ - const OVERWRITE = 'overwrite'; - -/** - * Skip scheme for Folder::copy - * if a subfolder with the same name exists it will be skipped - * - * @var string - */ - const SKIP = 'skip'; - -/** - * Sort mode by name - */ - const SORT_NAME = 'name'; - -/** - * Sort mode by time - */ - const SORT_TIME = 'time'; - -/** - * Path to Folder. - * - * @var string - * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#Folder::$path - */ - public $path = null; - -/** - * Sortedness. Whether or not list results - * should be sorted by name. - * - * @var bool - * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#Folder::$sort - */ - public $sort = false; - -/** - * Mode to be used on create. Does nothing on Windows platforms. - * - * @var int - * https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#Folder::$mode - */ - public $mode = 0755; - -/** - * Functions array to be called depending on the sort type chosen. - */ - protected $_fsorts = array( - self::SORT_NAME => 'getPathname', - self::SORT_TIME => 'getCTime' - ); - -/** - * Holds messages from last method. - * - * @var array - */ - protected $_messages = array(); - -/** - * Holds errors from last method. - * - * @var array - */ - protected $_errors = array(); - -/** - * Holds array of complete directory paths. - * - * @var array - */ - protected $_directories; - -/** - * Holds array of complete file paths. - * - * @var array - */ - protected $_files; - -/** - * Constructor. - * - * @param string $path Path to folder - * @param bool $create Create folder if not found - * @param int|bool $mode Mode (CHMOD) to apply to created folder, false to ignore - * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#Folder - */ - public function __construct($path = false, $create = false, $mode = false) { - if (empty($path)) { - $path = TMP; - } - if ($mode) { - $this->mode = $mode; - } - - if (!file_exists($path) && $create === true) { - $this->create($path, $this->mode); - } - if (!Folder::isAbsolute($path)) { - $path = realpath($path); - } - if (!empty($path)) { - $this->cd($path); - } - } - -/** - * Return current path. - * - * @return string Current path - * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#Folder::pwd - */ - public function pwd() { - return $this->path; - } - -/** - * Change directory to $path. - * - * @param string $path Path to the directory to change to - * @return string The new path. Returns false on failure - * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#Folder::cd - */ - public function cd($path) { - $path = $this->realpath($path); - if (is_dir($path)) { - return $this->path = $path; - } - return false; - } - -/** - * Returns an array of the contents of the current directory. - * The returned array holds two arrays: One of directories and one of files. - * - * @param string|bool $sort Whether you want the results sorted, set this and the sort property - * to false to get unsorted results. - * @param array|bool $exceptions Either an array or boolean true will not grab dot files - * @param bool $fullPath True returns the full path - * @return mixed Contents of current directory as an array, an empty array on failure - * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#Folder::read - */ - public function read($sort = self::SORT_NAME, $exceptions = false, $fullPath = false) { - $dirs = $files = array(); - - if (!$this->pwd()) { - return array($dirs, $files); - } - if (is_array($exceptions)) { - $exceptions = array_flip($exceptions); - } - $skipHidden = isset($exceptions['.']) || $exceptions === true; - - try { - $iterator = new DirectoryIterator($this->path); - } catch (Exception $e) { - return array($dirs, $files); - } - if (!is_bool($sort) && isset($this->_fsorts[$sort])) { - $methodName = $this->_fsorts[$sort]; - } else { - $methodName = $this->_fsorts[self::SORT_NAME]; - } - - foreach ($iterator as $item) { - if ($item->isDot()) { - continue; - } - $name = $item->getFileName(); - if ($skipHidden && $name[0] === '.' || isset($exceptions[$name])) { - continue; - } - if ($fullPath) { - $name = $item->getPathName(); - } - if ($item->isDir()) { - $dirs[$item->{$methodName}()][] = $name; - } else { - $files[$item->{$methodName}()][] = $name; - } - } - - if ($sort || $this->sort) { - ksort($dirs); - ksort($files); - } - - if ($dirs) { - $dirs = call_user_func_array('array_merge', $dirs); - } - if ($files) { - $files = call_user_func_array('array_merge', $files); - } - return array($dirs, $files); - } - -/** - * Returns an array of all matching files in current directory. - * - * @param string $regexpPattern Preg_match pattern (Defaults to: .*) - * @param bool $sort Whether results should be sorted. - * @return array Files that match given pattern - * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#Folder::find - */ - public function find($regexpPattern = '.*', $sort = false) { - list(, $files) = $this->read($sort); - return array_values(preg_grep('/^' . $regexpPattern . '$/i', $files)); - } - -/** - * Returns an array of all matching files in and below current directory. - * - * @param string $pattern Preg_match pattern (Defaults to: .*) - * @param bool $sort Whether results should be sorted. - * @return array Files matching $pattern - * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#Folder::findRecursive - */ - public function findRecursive($pattern = '.*', $sort = false) { - if (!$this->pwd()) { - return array(); - } - $startsOn = $this->path; - $out = $this->_findRecursive($pattern, $sort); - $this->cd($startsOn); - return $out; - } - -/** - * Private helper function for findRecursive. - * - * @param string $pattern Pattern to match against - * @param bool $sort Whether results should be sorted. - * @return array Files matching pattern - */ - protected function _findRecursive($pattern, $sort = false) { - list($dirs, $files) = $this->read($sort); - $found = array(); - - foreach ($files as $file) { - if (preg_match('/^' . $pattern . '$/i', $file)) { - $found[] = Folder::addPathElement($this->path, $file); - } - } - $start = $this->path; - - foreach ($dirs as $dir) { - $this->cd(Folder::addPathElement($start, $dir)); - $found = array_merge($found, $this->findRecursive($pattern, $sort)); - } - return $found; - } - -/** - * Returns true if given $path is a Windows path. - * - * @param string $path Path to check - * @return bool true if Windows path, false otherwise - * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#Folder::isWindowsPath - */ - public static function isWindowsPath($path) { - return (preg_match('/^[A-Z]:\\\\/i', $path) || substr($path, 0, 2) === '\\\\'); - } - -/** - * Returns true if given $path is an absolute path. - * - * @param string $path Path to check - * @return bool true if path is absolute. - * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#Folder::isAbsolute - */ - public static function isAbsolute($path) { - if (empty($path)) { - return false; - } - - return $path[0] === '/' || - preg_match('/^[A-Z]:\\\\/i', $path) || - substr($path, 0, 2) === '\\\\' || - static::isRegisteredStreamWrapper($path); - } - -/** - * Returns true if given $path is a registered stream wrapper. - * - * @param string $path Path to check - * @return bool true If path is registered stream wrapper. - */ - public static function isRegisteredStreamWrapper($path) { - if (preg_match('/^[A-Z]+(?=:\/\/)/i', $path, $matches) && - in_array($matches[0], stream_get_wrappers()) - ) { - return true; - } - return false; - } - -/** - * Returns a correct set of slashes for given $path. (\\ for Windows paths and / for other paths.) - * - * @param string $path Path to check - * @return string Set of slashes ("\\" or "/") - * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#Folder::normalizePath - */ - public static function normalizePath($path) { - return Folder::correctSlashFor($path); - } - -/** - * Returns a correct set of slashes for given $path. (\\ for Windows paths and / for other paths.) - * - * @param string $path Path to check - * @return string Set of slashes ("\\" or "/") - * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#Folder::correctSlashFor - */ - public static function correctSlashFor($path) { - return (Folder::isWindowsPath($path)) ? '\\' : '/'; - } - -/** - * Returns $path with added terminating slash (corrected for Windows or other OS). - * - * @param string $path Path to check - * @return string Path with ending slash - * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#Folder::slashTerm - */ - public static function slashTerm($path) { - if (Folder::isSlashTerm($path)) { - return $path; - } - return $path . Folder::correctSlashFor($path); - } - -/** - * Returns $path with $element added, with correct slash in-between. - * - * @param string $path Path - * @param string|array $element Element to add at end of path - * @return string Combined path - * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#Folder::addPathElement - */ - public static function addPathElement($path, $element) { - $element = (array)$element; - array_unshift($element, rtrim($path, DS)); - return implode(DS, $element); - } - -/** - * Returns true if the Folder is in the given Cake path. - * - * @param string $path The path to check. - * @return bool - * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#Folder::inCakePath - */ - public function inCakePath($path = '') { - $dir = substr(Folder::slashTerm(ROOT), 0, -1); - $newdir = $dir . $path; - - return $this->inPath($newdir); - } - -/** - * Returns true if the Folder is in the given path. - * - * @param string $path The absolute path to check that the current `pwd()` resides within. - * @param bool $reverse Reverse the search, check if the given `$path` resides within the current `pwd()`. - * @return bool - * @throws \InvalidArgumentException When the given `$path` argument is not an absolute path. - * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#Folder::inPath - */ - public function inPath($path = '', $reverse = false) { - if (!Folder::isAbsolute($path)) { - throw new InvalidArgumentException(__d('cake_dev', 'The $path argument is expected to be an absolute path.')); - } - - $dir = Folder::slashTerm($path); - $current = Folder::slashTerm($this->pwd()); - - if (!$reverse) { - $return = preg_match('/^' . preg_quote($dir, '/') . '(.*)/', $current); - } else { - $return = preg_match('/^' . preg_quote($current, '/') . '(.*)/', $dir); - } - return (bool)$return; - } - -/** - * Change the mode on a directory structure recursively. This includes changing the mode on files as well. - * - * @param string $path The path to chmod. - * @param int $mode Octal value, e.g. 0755. - * @param bool $recursive Chmod recursively, set to false to only change the current directory. - * @param array $exceptions Array of files, directories to skip. - * @return bool Success. - * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#Folder::chmod - */ - public function chmod($path, $mode = false, $recursive = true, $exceptions = array()) { - if (!$mode) { - $mode = $this->mode; - } - - if ($recursive === false && is_dir($path)) { - //@codingStandardsIgnoreStart - if (@chmod($path, intval($mode, 8))) { - //@codingStandardsIgnoreEnd - $this->_messages[] = __d('cake_dev', '%s changed to %s', $path, $mode); - return true; - } - - $this->_errors[] = __d('cake_dev', '%s NOT changed to %s', $path, $mode); - return false; - } - - if (is_dir($path)) { - $paths = $this->tree($path); - - foreach ($paths as $type) { - foreach ($type as $fullpath) { - $check = explode(DS, $fullpath); - $count = count($check); - - if (in_array($check[$count - 1], $exceptions)) { - continue; - } - - //@codingStandardsIgnoreStart - if (@chmod($fullpath, intval($mode, 8))) { - //@codingStandardsIgnoreEnd - $this->_messages[] = __d('cake_dev', '%s changed to %s', $fullpath, $mode); - } else { - $this->_errors[] = __d('cake_dev', '%s NOT changed to %s', $fullpath, $mode); - } - } - } - - if (empty($this->_errors)) { - return true; - } - } - return false; - } - -/** - * Returns an array of nested directories and files in each directory - * - * @param string $path the directory path to build the tree from - * @param array|bool $exceptions Either an array of files/folder to exclude - * or boolean true to not grab dot files/folders - * @param string $type either 'file' or 'dir'. null returns both files and directories - * @return mixed array of nested directories and files in each directory - * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#Folder::tree - */ - public function tree($path = null, $exceptions = false, $type = null) { - if (!$path) { - $path = $this->path; - } - $files = array(); - $directories = array($path); - - if (is_array($exceptions)) { - $exceptions = array_flip($exceptions); - } - $skipHidden = false; - if ($exceptions === true) { - $skipHidden = true; - } elseif (isset($exceptions['.'])) { - $skipHidden = true; - unset($exceptions['.']); - } - - try { - $directory = new RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::KEY_AS_PATHNAME | RecursiveDirectoryIterator::CURRENT_AS_SELF); - $iterator = new RecursiveIteratorIterator($directory, RecursiveIteratorIterator::SELF_FIRST); - } catch (Exception $e) { - if ($type === null) { - return array(array(), array()); - } - return array(); - } - - foreach ($iterator as $itemPath => $fsIterator) { - if ($skipHidden) { - $subPathName = $fsIterator->getSubPathname(); - if ($subPathName{0} === '.' || strpos($subPathName, DS . '.') !== false) { - continue; - } - } - $item = $fsIterator->current(); - if (!empty($exceptions) && isset($exceptions[$item->getFilename()])) { - continue; - } - - if ($item->isFile()) { - $files[] = $itemPath; - } elseif ($item->isDir() && !$item->isDot()) { - $directories[] = $itemPath; - } - } - if ($type === null) { - return array($directories, $files); - } - if ($type === 'dir') { - return $directories; - } - return $files; - } - -/** - * Create a directory structure recursively. - * - * Can be used to create deep path structures like `/foo/bar/baz/shoe/horn` - * - * @param string $pathname The directory structure to create. Either an absolute or relative - * path. If the path is relative and exists in the process' cwd it will not be created. - * Otherwise relative paths will be prefixed with the current pwd(). - * @param int $mode octal value 0755 - * @return bool Returns TRUE on success, FALSE on failure - * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#Folder::create - */ - public function create($pathname, $mode = false) { - if (is_dir($pathname) || empty($pathname)) { - return true; - } - - if (!static::isAbsolute($pathname)) { - $pathname = static::addPathElement($this->pwd(), $pathname); - } - - if (!$mode) { - $mode = $this->mode; - } - - if (is_file($pathname)) { - $this->_errors[] = __d('cake_dev', '%s is a file', $pathname); - return false; - } - $pathname = rtrim($pathname, DS); - $nextPathname = substr($pathname, 0, strrpos($pathname, DS)); - - if ($this->create($nextPathname, $mode)) { - if (!file_exists($pathname)) { - $old = umask(0); - if (mkdir($pathname, $mode)) { - umask($old); - $this->_messages[] = __d('cake_dev', '%s created', $pathname); - return true; - } - umask($old); - $this->_errors[] = __d('cake_dev', '%s NOT created', $pathname); - return false; - } - } - return false; - } - -/** - * Returns the size in bytes of this Folder and its contents. - * - * @return int size in bytes of current folder - * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#Folder::dirsize - */ - public function dirsize() { - $size = 0; - $directory = Folder::slashTerm($this->path); - $stack = array($directory); - $count = count($stack); - for ($i = 0, $j = $count; $i < $j; ++$i) { - if (is_file($stack[$i])) { - $size += filesize($stack[$i]); - } elseif (is_dir($stack[$i])) { - $dir = dir($stack[$i]); - if ($dir) { - while (false !== ($entry = $dir->read())) { - if ($entry === '.' || $entry === '..') { - continue; - } - $add = $stack[$i] . $entry; - - if (is_dir($stack[$i] . $entry)) { - $add = Folder::slashTerm($add); - } - $stack[] = $add; - } - $dir->close(); - } - } - $j = count($stack); - } - return $size; - } - -/** - * Recursively Remove directories if the system allows. - * - * @param string $path Path of directory to delete - * @return bool Success - * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#Folder::delete - */ - public function delete($path = null) { - if (!$path) { - $path = $this->pwd(); - } - if (!$path) { - return false; - } - $path = Folder::slashTerm($path); - if (is_dir($path)) { - try { - $directory = new RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::CURRENT_AS_SELF); - $iterator = new RecursiveIteratorIterator($directory, RecursiveIteratorIterator::CHILD_FIRST); - } catch (Exception $e) { - return false; - } - - foreach ($iterator as $item) { - $filePath = $item->getPathname(); - if ($item->isFile() || $item->isLink()) { - //@codingStandardsIgnoreStart - if (@unlink($filePath)) { - //@codingStandardsIgnoreEnd - $this->_messages[] = __d('cake_dev', '%s removed', $filePath); - } else { - $this->_errors[] = __d('cake_dev', '%s NOT removed', $filePath); - } - } elseif ($item->isDir() && !$item->isDot()) { - //@codingStandardsIgnoreStart - if (@rmdir($filePath)) { - //@codingStandardsIgnoreEnd - $this->_messages[] = __d('cake_dev', '%s removed', $filePath); - } else { - $this->_errors[] = __d('cake_dev', '%s NOT removed', $filePath); - return false; - } - } - } - - $path = rtrim($path, DS); - //@codingStandardsIgnoreStart - if (@rmdir($path)) { - //@codingStandardsIgnoreEnd - $this->_messages[] = __d('cake_dev', '%s removed', $path); - } else { - $this->_errors[] = __d('cake_dev', '%s NOT removed', $path); - return false; - } - } - return true; - } - -/** - * Recursive directory copy. - * - * ### Options - * - * - `to` The directory to copy to. - * - `from` The directory to copy from, this will cause a cd() to occur, changing the results of pwd(). - * - `mode` The mode to copy the files/directories with as integer, e.g. 0775. - * - `skip` Files/directories to skip. - * - `scheme` Folder::MERGE, Folder::OVERWRITE, Folder::SKIP - * - * @param array|string $options Either an array of options (see above) or a string of the destination directory. - * @return bool Success. - * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#Folder::copy - */ - public function copy($options) { - if (!$this->pwd()) { - return false; - } - $to = null; - if (is_string($options)) { - $to = $options; - $options = array(); - } - $options += array( - 'to' => $to, - 'from' => $this->path, - 'mode' => $this->mode, - 'skip' => array(), - 'scheme' => Folder::MERGE - ); - - $fromDir = $options['from']; - $toDir = $options['to']; - $mode = $options['mode']; - - if (!$this->cd($fromDir)) { - $this->_errors[] = __d('cake_dev', '%s not found', $fromDir); - return false; - } - - if (!is_dir($toDir)) { - $this->create($toDir, $mode); - } - - if (!is_writable($toDir)) { - $this->_errors[] = __d('cake_dev', '%s not writable', $toDir); - return false; - } - - $exceptions = array_merge(array('.', '..', '.svn'), $options['skip']); - //@codingStandardsIgnoreStart - if ($handle = @opendir($fromDir)) { - //@codingStandardsIgnoreEnd - while (($item = readdir($handle)) !== false) { - $to = Folder::addPathElement($toDir, $item); - if (($options['scheme'] != Folder::SKIP || !is_dir($to)) && !in_array($item, $exceptions)) { - $from = Folder::addPathElement($fromDir, $item); - if (is_file($from) && (!is_file($to) || $options['scheme'] != Folder::SKIP)) { - if (copy($from, $to)) { - chmod($to, intval($mode, 8)); - touch($to, filemtime($from)); - $this->_messages[] = __d('cake_dev', '%s copied to %s', $from, $to); - } else { - $this->_errors[] = __d('cake_dev', '%s NOT copied to %s', $from, $to); - } - } - - if (is_dir($from) && file_exists($to) && $options['scheme'] === Folder::OVERWRITE) { - $this->delete($to); - } - - if (is_dir($from) && !file_exists($to)) { - $old = umask(0); - if (mkdir($to, $mode)) { - umask($old); - $old = umask(0); - chmod($to, $mode); - umask($old); - $this->_messages[] = __d('cake_dev', '%s created', $to); - $options = array('to' => $to, 'from' => $from) + $options; - $this->copy($options); - } else { - $this->_errors[] = __d('cake_dev', '%s not created', $to); - } - } elseif (is_dir($from) && $options['scheme'] === Folder::MERGE) { - $options = array('to' => $to, 'from' => $from) + $options; - $this->copy($options); - } - } - } - closedir($handle); - } else { - return false; - } - - if (!empty($this->_errors)) { - return false; - } - return true; - } - -/** - * Recursive directory move. - * - * ### Options - * - * - `to` The directory to copy to. - * - `from` The directory to copy from, this will cause a cd() to occur, changing the results of pwd(). - * - `chmod` The mode to copy the files/directories with. - * - `skip` Files/directories to skip. - * - `scheme` Folder::MERGE, Folder::OVERWRITE, Folder::SKIP - * - * @param array $options (to, from, chmod, skip, scheme) - * @return bool Success - * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#Folder::move - */ - public function move($options) { - $to = null; - if (is_string($options)) { - $to = $options; - $options = (array)$options; - } - $options += array('to' => $to, 'from' => $this->path, 'mode' => $this->mode, 'skip' => array()); - - if ($this->copy($options)) { - if ($this->delete($options['from'])) { - return (bool)$this->cd($options['to']); - } - } - return false; - } - -/** - * get messages from latest method - * - * @param bool $reset Reset message stack after reading - * @return array - * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#Folder::messages - */ - public function messages($reset = true) { - $messages = $this->_messages; - if ($reset) { - $this->_messages = array(); - } - return $messages; - } - -/** - * get error from latest method - * - * @param bool $reset Reset error stack after reading - * @return array - * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#Folder::errors - */ - public function errors($reset = true) { - $errors = $this->_errors; - if ($reset) { - $this->_errors = array(); - } - return $errors; - } - -/** - * Get the real path (taking ".." and such into account) - * - * @param string $path Path to resolve - * @return string The resolved path - * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#Folder::realpath - */ - public function realpath($path) { - if (strpos($path, '..') === false) { - if (!Folder::isAbsolute($path)) { - $path = Folder::addPathElement($this->path, $path); - } - return $path; - } - $path = str_replace('/', DS, trim($path)); - $parts = explode(DS, $path); - $newparts = array(); - $newpath = ''; - if ($path[0] === DS) { - $newpath = DS; - } - - while (($part = array_shift($parts)) !== null) { - if ($part === '.' || $part === '') { - continue; - } - if ($part === '..') { - if (!empty($newparts)) { - array_pop($newparts); - continue; - } - return false; - } - $newparts[] = $part; - } - $newpath .= implode(DS, $newparts); - - return Folder::slashTerm($newpath); - } - -/** - * Returns true if given $path ends in a slash (i.e. is slash-terminated). - * - * @param string $path Path to check - * @return bool true if path ends with slash, false otherwise - * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#Folder::isSlashTerm - */ - public static function isSlashTerm($path) { - $lastChar = $path[strlen($path) - 1]; - return $lastChar === '/' || $lastChar === '\\'; - } +class Folder +{ + + /** + * Default scheme for Folder::copy + * Recursively merges subfolders with the same name + * + * @var string + */ + const MERGE = 'merge'; + + /** + * Overwrite scheme for Folder::copy + * subfolders with the same name will be replaced + * + * @var string + */ + const OVERWRITE = 'overwrite'; + + /** + * Skip scheme for Folder::copy + * if a subfolder with the same name exists it will be skipped + * + * @var string + */ + const SKIP = 'skip'; + + /** + * Sort mode by name + */ + const SORT_NAME = 'name'; + + /** + * Sort mode by time + */ + const SORT_TIME = 'time'; + + /** + * Path to Folder. + * + * @var string + * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#Folder::$path + */ + public $path = null; + + /** + * Sortedness. Whether or not list results + * should be sorted by name. + * + * @var bool + * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#Folder::$sort + */ + public $sort = false; + + /** + * Mode to be used on create. Does nothing on Windows platforms. + * + * @var int + * https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#Folder::$mode + */ + public $mode = 0755; + + /** + * Functions array to be called depending on the sort type chosen. + */ + protected $_fsorts = [ + self::SORT_NAME => 'getPathname', + self::SORT_TIME => 'getCTime' + ]; + + /** + * Holds messages from last method. + * + * @var array + */ + protected $_messages = []; + + /** + * Holds errors from last method. + * + * @var array + */ + protected $_errors = []; + + /** + * Holds array of complete directory paths. + * + * @var array + */ + protected $_directories; + + /** + * Holds array of complete file paths. + * + * @var array + */ + protected $_files; + + /** + * Constructor. + * + * @param string $path Path to folder + * @param bool $create Create folder if not found + * @param int|bool $mode Mode (CHMOD) to apply to created folder, false to ignore + * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#Folder + */ + public function __construct($path = false, $create = false, $mode = false) + { + if (empty($path)) { + $path = TMP; + } + if ($mode) { + $this->mode = $mode; + } + + if (!file_exists($path) && $create === true) { + $this->create($path, $this->mode); + } + if (!Folder::isAbsolute($path)) { + $path = realpath($path); + } + if (!empty($path)) { + $this->cd($path); + } + } + + /** + * Create a directory structure recursively. + * + * Can be used to create deep path structures like `/foo/bar/baz/shoe/horn` + * + * @param string $pathname The directory structure to create. Either an absolute or relative + * path. If the path is relative and exists in the process' cwd it will not be created. + * Otherwise relative paths will be prefixed with the current pwd(). + * @param int $mode octal value 0755 + * @return bool Returns TRUE on success, FALSE on failure + * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#Folder::create + */ + public function create($pathname, $mode = false) + { + if (is_dir($pathname) || empty($pathname)) { + return true; + } + + if (!static::isAbsolute($pathname)) { + $pathname = static::addPathElement($this->pwd(), $pathname); + } + + if (!$mode) { + $mode = $this->mode; + } + + if (is_file($pathname)) { + $this->_errors[] = __d('cake_dev', '%s is a file', $pathname); + return false; + } + $pathname = rtrim($pathname, DS); + $nextPathname = substr($pathname, 0, strrpos($pathname, DS)); + + if ($this->create($nextPathname, $mode)) { + if (!file_exists($pathname)) { + $old = umask(0); + if (mkdir($pathname, $mode)) { + umask($old); + $this->_messages[] = __d('cake_dev', '%s created', $pathname); + return true; + } + umask($old); + $this->_errors[] = __d('cake_dev', '%s NOT created', $pathname); + return false; + } + } + return false; + } + + /** + * Returns true if given $path is an absolute path. + * + * @param string $path Path to check + * @return bool true if path is absolute. + * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#Folder::isAbsolute + */ + public static function isAbsolute($path) + { + if (empty($path)) { + return false; + } + + return $path[0] === '/' || + preg_match('/^[A-Z]:\\\\/i', $path) || + substr($path, 0, 2) === '\\\\' || + static::isRegisteredStreamWrapper($path); + } + + /** + * Returns true if given $path is a registered stream wrapper. + * + * @param string $path Path to check + * @return bool true If path is registered stream wrapper. + */ + public static function isRegisteredStreamWrapper($path) + { + if (preg_match('/^[A-Z]+(?=:\/\/)/i', $path, $matches) && + in_array($matches[0], stream_get_wrappers()) + ) { + return true; + } + return false; + } + + /** + * Returns $path with $element added, with correct slash in-between. + * + * @param string $path Path + * @param string|array $element Element to add at end of path + * @return string Combined path + * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#Folder::addPathElement + */ + public static function addPathElement($path, $element) + { + $element = (array)$element; + array_unshift($element, rtrim($path, DS)); + return implode(DS, $element); + } + + /** + * Return current path. + * + * @return string Current path + * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#Folder::pwd + */ + public function pwd() + { + return $this->path; + } + + /** + * Change directory to $path. + * + * @param string $path Path to the directory to change to + * @return string The new path. Returns false on failure + * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#Folder::cd + */ + public function cd($path) + { + $path = $this->realpath($path); + if (is_dir($path)) { + return $this->path = $path; + } + return false; + } + + /** + * Get the real path (taking ".." and such into account) + * + * @param string $path Path to resolve + * @return string The resolved path + * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#Folder::realpath + */ + public function realpath($path) + { + if (strpos($path, '..') === false) { + if (!Folder::isAbsolute($path)) { + $path = Folder::addPathElement($this->path, $path); + } + return $path; + } + $path = str_replace('/', DS, trim($path)); + $parts = explode(DS, $path); + $newparts = []; + $newpath = ''; + if ($path[0] === DS) { + $newpath = DS; + } + + while (($part = array_shift($parts)) !== null) { + if ($part === '.' || $part === '') { + continue; + } + if ($part === '..') { + if (!empty($newparts)) { + array_pop($newparts); + continue; + } + return false; + } + $newparts[] = $part; + } + $newpath .= implode(DS, $newparts); + + return Folder::slashTerm($newpath); + } + + /** + * Returns $path with added terminating slash (corrected for Windows or other OS). + * + * @param string $path Path to check + * @return string Path with ending slash + * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#Folder::slashTerm + */ + public static function slashTerm($path) + { + if (Folder::isSlashTerm($path)) { + return $path; + } + return $path . Folder::correctSlashFor($path); + } + + /** + * Returns true if given $path ends in a slash (i.e. is slash-terminated). + * + * @param string $path Path to check + * @return bool true if path ends with slash, false otherwise + * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#Folder::isSlashTerm + */ + public static function isSlashTerm($path) + { + $lastChar = $path[strlen($path) - 1]; + return $lastChar === '/' || $lastChar === '\\'; + } + + /** + * Returns a correct set of slashes for given $path. (\\ for Windows paths and / for other paths.) + * + * @param string $path Path to check + * @return string Set of slashes ("\\" or "/") + * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#Folder::correctSlashFor + */ + public static function correctSlashFor($path) + { + return (Folder::isWindowsPath($path)) ? '\\' : '/'; + } + + /** + * Returns true if given $path is a Windows path. + * + * @param string $path Path to check + * @return bool true if Windows path, false otherwise + * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#Folder::isWindowsPath + */ + public static function isWindowsPath($path) + { + return (preg_match('/^[A-Z]:\\\\/i', $path) || substr($path, 0, 2) === '\\\\'); + } + + /** + * Returns a correct set of slashes for given $path. (\\ for Windows paths and / for other paths.) + * + * @param string $path Path to check + * @return string Set of slashes ("\\" or "/") + * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#Folder::normalizePath + */ + public static function normalizePath($path) + { + return Folder::correctSlashFor($path); + } + + /** + * Returns an array of all matching files in current directory. + * + * @param string $regexpPattern Preg_match pattern (Defaults to: .*) + * @param bool $sort Whether results should be sorted. + * @return array Files that match given pattern + * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#Folder::find + */ + public function find($regexpPattern = '.*', $sort = false) + { + list(, $files) = $this->read($sort); + return array_values(preg_grep('/^' . $regexpPattern . '$/i', $files)); + } + + /** + * Returns an array of the contents of the current directory. + * The returned array holds two arrays: One of directories and one of files. + * + * @param string|bool $sort Whether you want the results sorted, set this and the sort property + * to false to get unsorted results. + * @param array|bool $exceptions Either an array or boolean true will not grab dot files + * @param bool $fullPath True returns the full path + * @return mixed Contents of current directory as an array, an empty array on failure + * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#Folder::read + */ + public function read($sort = self::SORT_NAME, $exceptions = false, $fullPath = false) + { + $dirs = $files = []; + + if (!$this->pwd()) { + return [$dirs, $files]; + } + if (is_array($exceptions)) { + $exceptions = array_flip($exceptions); + } + $skipHidden = isset($exceptions['.']) || $exceptions === true; + + try { + $iterator = new DirectoryIterator($this->path); + } catch (Exception $e) { + return [$dirs, $files]; + } + if (!is_bool($sort) && isset($this->_fsorts[$sort])) { + $methodName = $this->_fsorts[$sort]; + } else { + $methodName = $this->_fsorts[self::SORT_NAME]; + } + + foreach ($iterator as $item) { + if ($item->isDot()) { + continue; + } + $name = $item->getFileName(); + if ($skipHidden && $name[0] === '.' || isset($exceptions[$name])) { + continue; + } + if ($fullPath) { + $name = $item->getPathName(); + } + if ($item->isDir()) { + $dirs[$item->{$methodName}()][] = $name; + } else { + $files[$item->{$methodName}()][] = $name; + } + } + + if ($sort || $this->sort) { + ksort($dirs); + ksort($files); + } + + if ($dirs) { + $dirs = call_user_func_array('array_merge', $dirs); + } + if ($files) { + $files = call_user_func_array('array_merge', $files); + } + return [$dirs, $files]; + } + + /** + * Returns an array of all matching files in and below current directory. + * + * @param string $pattern Preg_match pattern (Defaults to: .*) + * @param bool $sort Whether results should be sorted. + * @return array Files matching $pattern + * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#Folder::findRecursive + */ + public function findRecursive($pattern = '.*', $sort = false) + { + if (!$this->pwd()) { + return []; + } + $startsOn = $this->path; + $out = $this->_findRecursive($pattern, $sort); + $this->cd($startsOn); + return $out; + } + + /** + * Private helper function for findRecursive. + * + * @param string $pattern Pattern to match against + * @param bool $sort Whether results should be sorted. + * @return array Files matching pattern + */ + protected function _findRecursive($pattern, $sort = false) + { + list($dirs, $files) = $this->read($sort); + $found = []; + + foreach ($files as $file) { + if (preg_match('/^' . $pattern . '$/i', $file)) { + $found[] = Folder::addPathElement($this->path, $file); + } + } + $start = $this->path; + + foreach ($dirs as $dir) { + $this->cd(Folder::addPathElement($start, $dir)); + $found = array_merge($found, $this->findRecursive($pattern, $sort)); + } + return $found; + } + + /** + * Returns true if the Folder is in the given Cake path. + * + * @param string $path The path to check. + * @return bool + * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#Folder::inCakePath + */ + public function inCakePath($path = '') + { + $dir = substr(Folder::slashTerm(ROOT), 0, -1); + $newdir = $dir . $path; + + return $this->inPath($newdir); + } + + /** + * Returns true if the Folder is in the given path. + * + * @param string $path The absolute path to check that the current `pwd()` resides within. + * @param bool $reverse Reverse the search, check if the given `$path` resides within the current `pwd()`. + * @return bool + * @throws InvalidArgumentException When the given `$path` argument is not an absolute path. + * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#Folder::inPath + */ + public function inPath($path = '', $reverse = false) + { + if (!Folder::isAbsolute($path)) { + throw new InvalidArgumentException(__d('cake_dev', 'The $path argument is expected to be an absolute path.')); + } + + $dir = Folder::slashTerm($path); + $current = Folder::slashTerm($this->pwd()); + + if (!$reverse) { + $return = preg_match('/^' . preg_quote($dir, '/') . '(.*)/', $current); + } else { + $return = preg_match('/^' . preg_quote($current, '/') . '(.*)/', $dir); + } + return (bool)$return; + } + + /** + * Change the mode on a directory structure recursively. This includes changing the mode on files as well. + * + * @param string $path The path to chmod. + * @param int $mode Octal value, e.g. 0755. + * @param bool $recursive Chmod recursively, set to false to only change the current directory. + * @param array $exceptions Array of files, directories to skip. + * @return bool Success. + * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#Folder::chmod + */ + public function chmod($path, $mode = false, $recursive = true, $exceptions = []) + { + if (!$mode) { + $mode = $this->mode; + } + + if ($recursive === false && is_dir($path)) { + //@codingStandardsIgnoreStart + if (@chmod($path, intval($mode, 8))) { + //@codingStandardsIgnoreEnd + $this->_messages[] = __d('cake_dev', '%s changed to %s', $path, $mode); + return true; + } + + $this->_errors[] = __d('cake_dev', '%s NOT changed to %s', $path, $mode); + return false; + } + + if (is_dir($path)) { + $paths = $this->tree($path); + + foreach ($paths as $type) { + foreach ($type as $fullpath) { + $check = explode(DS, $fullpath); + $count = count($check); + + if (in_array($check[$count - 1], $exceptions)) { + continue; + } + + //@codingStandardsIgnoreStart + if (@chmod($fullpath, intval($mode, 8))) { + //@codingStandardsIgnoreEnd + $this->_messages[] = __d('cake_dev', '%s changed to %s', $fullpath, $mode); + } else { + $this->_errors[] = __d('cake_dev', '%s NOT changed to %s', $fullpath, $mode); + } + } + } + + if (empty($this->_errors)) { + return true; + } + } + return false; + } + + /** + * Returns an array of nested directories and files in each directory + * + * @param string $path the directory path to build the tree from + * @param array|bool $exceptions Either an array of files/folder to exclude + * or boolean true to not grab dot files/folders + * @param string $type either 'file' or 'dir'. null returns both files and directories + * @return mixed array of nested directories and files in each directory + * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#Folder::tree + */ + public function tree($path = null, $exceptions = false, $type = null) + { + if (!$path) { + $path = $this->path; + } + $files = []; + $directories = [$path]; + + if (is_array($exceptions)) { + $exceptions = array_flip($exceptions); + } + $skipHidden = false; + if ($exceptions === true) { + $skipHidden = true; + } else if (isset($exceptions['.'])) { + $skipHidden = true; + unset($exceptions['.']); + } + + try { + $directory = new RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::KEY_AS_PATHNAME | RecursiveDirectoryIterator::CURRENT_AS_SELF); + $iterator = new RecursiveIteratorIterator($directory, RecursiveIteratorIterator::SELF_FIRST); + } catch (Exception $e) { + if ($type === null) { + return [[], []]; + } + return []; + } + + foreach ($iterator as $itemPath => $fsIterator) { + if ($skipHidden) { + $subPathName = $fsIterator->getSubPathname(); + if ($subPathName{0} === '.' || strpos($subPathName, DS . '.') !== false) { + continue; + } + } + $item = $fsIterator->current(); + if (!empty($exceptions) && isset($exceptions[$item->getFilename()])) { + continue; + } + + if ($item->isFile()) { + $files[] = $itemPath; + } else if ($item->isDir() && !$item->isDot()) { + $directories[] = $itemPath; + } + } + if ($type === null) { + return [$directories, $files]; + } + if ($type === 'dir') { + return $directories; + } + return $files; + } + + /** + * Returns the size in bytes of this Folder and its contents. + * + * @return int size in bytes of current folder + * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#Folder::dirsize + */ + public function dirsize() + { + $size = 0; + $directory = Folder::slashTerm($this->path); + $stack = [$directory]; + $count = count($stack); + for ($i = 0, $j = $count; $i < $j; ++$i) { + if (is_file($stack[$i])) { + $size += filesize($stack[$i]); + } else if (is_dir($stack[$i])) { + $dir = dir($stack[$i]); + if ($dir) { + while (false !== ($entry = $dir->read())) { + if ($entry === '.' || $entry === '..') { + continue; + } + $add = $stack[$i] . $entry; + + if (is_dir($stack[$i] . $entry)) { + $add = Folder::slashTerm($add); + } + $stack[] = $add; + } + $dir->close(); + } + } + $j = count($stack); + } + return $size; + } + + /** + * Recursive directory move. + * + * ### Options + * + * - `to` The directory to copy to. + * - `from` The directory to copy from, this will cause a cd() to occur, changing the results of pwd(). + * - `chmod` The mode to copy the files/directories with. + * - `skip` Files/directories to skip. + * - `scheme` Folder::MERGE, Folder::OVERWRITE, Folder::SKIP + * + * @param array $options (to, from, chmod, skip, scheme) + * @return bool Success + * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#Folder::move + */ + public function move($options) + { + $to = null; + if (is_string($options)) { + $to = $options; + $options = (array)$options; + } + $options += ['to' => $to, 'from' => $this->path, 'mode' => $this->mode, 'skip' => []]; + + if ($this->copy($options)) { + if ($this->delete($options['from'])) { + return (bool)$this->cd($options['to']); + } + } + return false; + } + + /** + * Recursive directory copy. + * + * ### Options + * + * - `to` The directory to copy to. + * - `from` The directory to copy from, this will cause a cd() to occur, changing the results of pwd(). + * - `mode` The mode to copy the files/directories with as integer, e.g. 0775. + * - `skip` Files/directories to skip. + * - `scheme` Folder::MERGE, Folder::OVERWRITE, Folder::SKIP + * + * @param array|string $options Either an array of options (see above) or a string of the destination directory. + * @return bool Success. + * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#Folder::copy + */ + public function copy($options) + { + if (!$this->pwd()) { + return false; + } + $to = null; + if (is_string($options)) { + $to = $options; + $options = []; + } + $options += [ + 'to' => $to, + 'from' => $this->path, + 'mode' => $this->mode, + 'skip' => [], + 'scheme' => Folder::MERGE + ]; + + $fromDir = $options['from']; + $toDir = $options['to']; + $mode = $options['mode']; + + if (!$this->cd($fromDir)) { + $this->_errors[] = __d('cake_dev', '%s not found', $fromDir); + return false; + } + + if (!is_dir($toDir)) { + $this->create($toDir, $mode); + } + + if (!is_writable($toDir)) { + $this->_errors[] = __d('cake_dev', '%s not writable', $toDir); + return false; + } + + $exceptions = array_merge(['.', '..', '.svn'], $options['skip']); + //@codingStandardsIgnoreStart + if ($handle = @opendir($fromDir)) { + //@codingStandardsIgnoreEnd + while (($item = readdir($handle)) !== false) { + $to = Folder::addPathElement($toDir, $item); + if (($options['scheme'] != Folder::SKIP || !is_dir($to)) && !in_array($item, $exceptions)) { + $from = Folder::addPathElement($fromDir, $item); + if (is_file($from) && (!is_file($to) || $options['scheme'] != Folder::SKIP)) { + if (copy($from, $to)) { + chmod($to, intval($mode, 8)); + touch($to, filemtime($from)); + $this->_messages[] = __d('cake_dev', '%s copied to %s', $from, $to); + } else { + $this->_errors[] = __d('cake_dev', '%s NOT copied to %s', $from, $to); + } + } + + if (is_dir($from) && file_exists($to) && $options['scheme'] === Folder::OVERWRITE) { + $this->delete($to); + } + + if (is_dir($from) && !file_exists($to)) { + $old = umask(0); + if (mkdir($to, $mode)) { + umask($old); + $old = umask(0); + chmod($to, $mode); + umask($old); + $this->_messages[] = __d('cake_dev', '%s created', $to); + $options = ['to' => $to, 'from' => $from] + $options; + $this->copy($options); + } else { + $this->_errors[] = __d('cake_dev', '%s not created', $to); + } + } else if (is_dir($from) && $options['scheme'] === Folder::MERGE) { + $options = ['to' => $to, 'from' => $from] + $options; + $this->copy($options); + } + } + } + closedir($handle); + } else { + return false; + } + + if (!empty($this->_errors)) { + return false; + } + return true; + } + + /** + * Recursively Remove directories if the system allows. + * + * @param string $path Path of directory to delete + * @return bool Success + * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#Folder::delete + */ + public function delete($path = null) + { + if (!$path) { + $path = $this->pwd(); + } + if (!$path) { + return false; + } + $path = Folder::slashTerm($path); + if (is_dir($path)) { + try { + $directory = new RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::CURRENT_AS_SELF); + $iterator = new RecursiveIteratorIterator($directory, RecursiveIteratorIterator::CHILD_FIRST); + } catch (Exception $e) { + return false; + } + + foreach ($iterator as $item) { + $filePath = $item->getPathname(); + if ($item->isFile() || $item->isLink()) { + //@codingStandardsIgnoreStart + if (@unlink($filePath)) { + //@codingStandardsIgnoreEnd + $this->_messages[] = __d('cake_dev', '%s removed', $filePath); + } else { + $this->_errors[] = __d('cake_dev', '%s NOT removed', $filePath); + } + } else if ($item->isDir() && !$item->isDot()) { + //@codingStandardsIgnoreStart + if (@rmdir($filePath)) { + //@codingStandardsIgnoreEnd + $this->_messages[] = __d('cake_dev', '%s removed', $filePath); + } else { + $this->_errors[] = __d('cake_dev', '%s NOT removed', $filePath); + return false; + } + } + } + + $path = rtrim($path, DS); + //@codingStandardsIgnoreStart + if (@rmdir($path)) { + //@codingStandardsIgnoreEnd + $this->_messages[] = __d('cake_dev', '%s removed', $path); + } else { + $this->_errors[] = __d('cake_dev', '%s NOT removed', $path); + return false; + } + } + return true; + } + + /** + * get messages from latest method + * + * @param bool $reset Reset message stack after reading + * @return array + * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#Folder::messages + */ + public function messages($reset = true) + { + $messages = $this->_messages; + if ($reset) { + $this->_messages = []; + } + return $messages; + } + + /** + * get error from latest method + * + * @param bool $reset Reset error stack after reading + * @return array + * @link https://book.cakephp.org/2.0/en/core-utility-libraries/file-folder.html#Folder::errors + */ + public function errors($reset = true) + { + $errors = $this->_errors; + if ($reset) { + $this->_errors = []; + } + return $errors; + } } diff --git a/lib/Cake/Utility/Hash.php b/lib/Cake/Utility/Hash.php index 84c706ae..07b5cefd 100755 --- a/lib/Cake/Utility/Hash.php +++ b/lib/Cake/Utility/Hash.php @@ -27,1119 +27,1149 @@ * * @package Cake.Utility */ -class Hash { - -/** - * Get a single value specified by $path out of $data. - * Does not support the full dot notation feature set, - * but is faster for simple read operations. - * - * @param array $data Array of data to operate on. - * @param string|array $path The path being searched for. Either a dot - * separated string, or an array of path segments. - * @param mixed $default The return value when the path does not exist - * @throws InvalidArgumentException - * @return mixed The value fetched from the array, or null. - * @link https://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::get - */ - public static function get(array $data, $path, $default = null) { - if (empty($data) || $path === null) { - return $default; - } - if (is_string($path) || is_numeric($path)) { - $parts = explode('.', $path); - } elseif (is_bool($path) || $path === null) { - $parts = array($path); - } else { - if (!is_array($path)) { - throw new InvalidArgumentException(__d('cake_dev', - 'Invalid path parameter: %s, should be dot separated path or array.', - var_export($path, true) - )); - } - $parts = $path; - } - - foreach ($parts as $key) { - if (is_array($data) && isset($data[$key])) { - $data =& $data[$key]; - } else { - return $default; - } - } - - return $data; - } - -/** - * Gets the values from an array matching the $path expression. - * The path expression is a dot separated expression, that can contain a set - * of patterns and expressions: - * - * - `{n}` Matches any numeric key, or integer. - * - `{s}` Matches any string key. - * - `{*}` Matches any value. - * - `Foo` Matches any key with the exact same value. - * - * There are a number of attribute operators: - * - * - `=`, `!=` Equality. - * - `>`, `<`, `>=`, `<=` Value comparison. - * - `=/.../` Regular expression pattern match. - * - * Given a set of User array data, from a `$User->find('all')` call: - * - * - `1.User.name` Get the name of the user at index 1. - * - `{n}.User.name` Get the name of every user in the set of users. - * - `{n}.User[id].name` Get the name of every user with an id key. - * - `{n}.User[id>=2].name` Get the name of every user with an id key greater than or equal to 2. - * - `{n}.User[username=/^paul/]` Get User elements with username matching `^paul`. - * - * @param array $data The data to extract from. - * @param string $path The path to extract. - * @return array An array of the extracted values. Returns an empty array - * if there are no matches. - * @link https://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::extract - */ - public static function extract(array $data, $path) { - if (empty($path)) { - return $data; - } - - // Simple paths. - if (!preg_match('/[{\[]/', $path)) { - return (array)static::get($data, $path); - } - - if (strpos($path, '[') === false) { - $tokens = explode('.', $path); - } else { - $tokens = CakeText::tokenize($path, '.', '[', ']'); - } - - $_key = '__set_item__'; - - $context = array($_key => array($data)); - - foreach ($tokens as $token) { - $next = array(); - - list($token, $conditions) = static::_splitConditions($token); - - foreach ($context[$_key] as $item) { - foreach ((array)$item as $k => $v) { - if (static::_matchToken($k, $token)) { - $next[] = $v; - } - } - } - - // Filter for attributes. - if ($conditions) { - $filter = array(); - foreach ($next as $item) { - if (is_array($item) && static::_matches($item, $conditions)) { - $filter[] = $item; - } - } - $next = $filter; - } - $context = array($_key => $next); - - } - return $context[$_key]; - } - -/** - * Split token conditions - * - * @param string $token the token being splitted. - * @return array array(token, conditions) with token splitted - */ - protected static function _splitConditions($token) { - $conditions = false; - $position = strpos($token, '['); - if ($position !== false) { - $conditions = substr($token, $position); - $token = substr($token, 0, $position); - } - - return array($token, $conditions); - } - -/** - * Check a key against a token. - * - * @param string $key The key in the array being searched. - * @param string $token The token being matched. - * @return bool - */ - protected static function _matchToken($key, $token) { - switch ($token) { - case '{n}': - return is_numeric($key); - case '{s}': - return is_string($key); - case '{*}': - return true; - default: - return is_numeric($token) ? ($key == $token) : $key === $token; - } - } - -/** - * Checks whether or not $data matches the attribute patterns - * - * @param array $data Array of data to match. - * @param string $selector The patterns to match. - * @return bool Fitness of expression. - */ - protected static function _matches(array $data, $selector) { - preg_match_all( - '/(\[ (?P[^=>[><]) \s* (?P(?:\/.*?\/ | [^\]]+)) )? \])/x', - $selector, - $conditions, - PREG_SET_ORDER - ); - - foreach ($conditions as $cond) { - $attr = $cond['attr']; - $op = isset($cond['op']) ? $cond['op'] : null; - $val = isset($cond['val']) ? $cond['val'] : null; - - // Presence test. - if (empty($op) && empty($val) && !isset($data[$attr])) { - return false; - } - - // Empty attribute = fail. - if (!(isset($data[$attr]) || array_key_exists($attr, $data))) { - return false; - } - - $prop = null; - if (isset($data[$attr])) { - $prop = $data[$attr]; - } - $isBool = is_bool($prop); - if ($isBool && is_numeric($val)) { - $prop = $prop ? '1' : '0'; - } elseif ($isBool) { - $prop = $prop ? 'true' : 'false'; - } - - // Pattern matches and other operators. - if ($op === '=' && $val && $val[0] === '/') { - if (!preg_match($val, $prop)) { - return false; - } - } elseif (($op === '=' && $prop != $val) || - ($op === '!=' && $prop == $val) || - ($op === '>' && $prop <= $val) || - ($op === '<' && $prop >= $val) || - ($op === '>=' && $prop < $val) || - ($op === '<=' && $prop > $val) - ) { - return false; - } - - } - return true; - } - -/** - * Insert $values into an array with the given $path. You can use - * `{n}` and `{s}` elements to insert $data multiple times. - * - * @param array $data The data to insert into. - * @param string $path The path to insert at. - * @param mixed $values The values to insert. - * @return array The data with $values inserted. - * @link https://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::insert - */ - public static function insert(array $data, $path, $values = null) { - if (strpos($path, '[') === false) { - $tokens = explode('.', $path); - } else { - $tokens = CakeText::tokenize($path, '.', '[', ']'); - } - - if (strpos($path, '{') === false && strpos($path, '[') === false) { - return static::_simpleOp('insert', $data, $tokens, $values); - } - - $token = array_shift($tokens); - $nextPath = implode('.', $tokens); - - list($token, $conditions) = static::_splitConditions($token); - - foreach ($data as $k => $v) { - if (static::_matchToken($k, $token)) { - if (!$conditions || static::_matches($v, $conditions)) { - $data[$k] = $nextPath - ? static::insert($v, $nextPath, $values) - : array_merge($v, (array)$values); - } - } - } - return $data; - } - -/** - * Perform a simple insert/remove operation. - * - * @param string $op The operation to do. - * @param array $data The data to operate on. - * @param array $path The path to work on. - * @param mixed $values The values to insert when doing inserts. - * @return array data. - */ - protected static function _simpleOp($op, $data, $path, $values = null) { - $_list =& $data; - - $count = count($path); - $last = $count - 1; - foreach ($path as $i => $key) { - if ($op === 'insert') { - if ($i === $last) { - $_list[$key] = $values; - return $data; - } - if (!isset($_list[$key])) { - $_list[$key] = array(); - } - $_list =& $_list[$key]; - if (!is_array($_list)) { - $_list = array(); - } - } elseif ($op === 'remove') { - if ($i === $last) { - if (is_array($_list)) { - unset($_list[$key]); - } - return $data; - } - if (!isset($_list[$key])) { - return $data; - } - $_list =& $_list[$key]; - } - } - } - -/** - * Remove data matching $path from the $data array. - * You can use `{n}` and `{s}` to remove multiple elements - * from $data. - * - * @param array $data The data to operate on - * @param string $path A path expression to use to remove. - * @return array The modified array. - * @link https://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::remove - */ - public static function remove(array $data, $path) { - if (strpos($path, '[') === false) { - $tokens = explode('.', $path); - } else { - $tokens = CakeText::tokenize($path, '.', '[', ']'); - } - - if (strpos($path, '{') === false && strpos($path, '[') === false) { - return static::_simpleOp('remove', $data, $tokens); - } - - $token = array_shift($tokens); - $nextPath = implode('.', $tokens); - - list($token, $conditions) = static::_splitConditions($token); - - foreach ($data as $k => $v) { - $match = static::_matchToken($k, $token); - if ($match && is_array($v)) { - if ($conditions) { - if (static::_matches($v, $conditions)) { - if ($nextPath !== '') { - $data[$k] = static::remove($v, $nextPath); - } else { - unset($data[$k]); - } - } - } else { - $data[$k] = static::remove($v, $nextPath); - } - if (empty($data[$k])) { - unset($data[$k]); - } - } elseif ($match && $nextPath === '') { - unset($data[$k]); - } - } - return $data; - } - -/** - * Creates an associative array using `$keyPath` as the path to build its keys, and optionally - * `$valuePath` as path to get the values. If `$valuePath` is not specified, all values will be initialized - * to null (useful for Hash::merge). You can optionally group the values by what is obtained when - * following the path specified in `$groupPath`. - * - * @param array $data Array from where to extract keys and values - * @param array|string $keyPath A dot-separated string or array for formatting rules. - * @param array|string $valuePath A dot-separated string or array for formatting rules. - * @param string $groupPath A dot-separated string. - * @return array Combined array - * @link https://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::combine - * @throws CakeException CakeException When keys and values count is unequal. - */ - public static function combine(array $data, $keyPath, $valuePath = null, $groupPath = null) { - if (empty($data)) { - return array(); - } - - if (is_array($keyPath)) { - $format = array_shift($keyPath); - $keys = static::format($data, $keyPath, $format); - } else { - $keys = static::extract($data, $keyPath); - } - if (empty($keys)) { - return array(); - } - - if (!empty($valuePath) && is_array($valuePath)) { - $format = array_shift($valuePath); - $vals = static::format($data, $valuePath, $format); - } elseif (!empty($valuePath)) { - $vals = static::extract($data, $valuePath); - } - if (empty($vals)) { - $vals = array_fill(0, count($keys), null); - } - - if (count($keys) !== count($vals)) { - throw new CakeException(__d( - 'cake_dev', - 'Hash::combine() needs an equal number of keys + values.' - )); - } - - if ($groupPath !== null) { - $group = static::extract($data, $groupPath); - if (!empty($group)) { - $c = count($keys); - for ($i = 0; $i < $c; $i++) { - if (!isset($group[$i])) { - $group[$i] = 0; - } - if (!isset($out[$group[$i]])) { - $out[$group[$i]] = array(); - } - $out[$group[$i]][$keys[$i]] = $vals[$i]; - } - return $out; - } - } - if (empty($vals)) { - return array(); - } - return array_combine($keys, $vals); - } - -/** - * Returns a formatted series of values extracted from `$data`, using - * `$format` as the format and `$paths` as the values to extract. - * - * Usage: - * - * ``` - * $result = Hash::format($users, array('{n}.User.id', '{n}.User.name'), '%s : %s'); - * ``` - * - * The `$format` string can use any format options that `vsprintf()` and `sprintf()` do. - * - * @param array $data Source array from which to extract the data - * @param array $paths An array containing one or more Hash::extract()-style key paths - * @param string $format Format string into which values will be inserted, see sprintf() - * @return array An array of strings extracted from `$path` and formatted with `$format` - * @link https://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::format - * @see sprintf() - * @see Hash::extract() - * @link https://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::format - */ - public static function format(array $data, array $paths, $format) { - $extracted = array(); - $count = count($paths); - - if (!$count) { - return null; - } - - for ($i = 0; $i < $count; $i++) { - $extracted[] = static::extract($data, $paths[$i]); - } - $out = array(); - $data = $extracted; - $count = count($data[0]); - - $countTwo = count($data); - for ($j = 0; $j < $count; $j++) { - $args = array(); - for ($i = 0; $i < $countTwo; $i++) { - if (array_key_exists($j, $data[$i])) { - $args[] = $data[$i][$j]; - } - } - $out[] = vsprintf($format, $args); - } - return $out; - } - -/** - * Determines if one array contains the exact keys and values of another. - * - * @param array $data The data to search through. - * @param array $needle The values to file in $data - * @return bool true if $data contains $needle, false otherwise - * @link https://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::contains - */ - public static function contains(array $data, array $needle) { - if (empty($data) || empty($needle)) { - return false; - } - $stack = array(); - - while (!empty($needle)) { - $key = key($needle); - $val = $needle[$key]; - unset($needle[$key]); - - if (array_key_exists($key, $data) && is_array($val)) { - $next = $data[$key]; - unset($data[$key]); - - if (!empty($val)) { - $stack[] = array($val, $next); - } - } elseif (!array_key_exists($key, $data) || $data[$key] != $val) { - return false; - } - - if (empty($needle) && !empty($stack)) { - list($needle, $data) = array_pop($stack); - } - } - return true; - } - -/** - * Test whether or not a given path exists in $data. - * This method uses the same path syntax as Hash::extract() - * - * Checking for paths that could target more than one element will - * make sure that at least one matching element exists. - * - * @param array $data The data to check. - * @param string $path The path to check for. - * @return bool Existence of path. - * @see Hash::extract() - * @link https://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::check - */ - public static function check(array $data, $path) { - $results = static::extract($data, $path); - if (!is_array($results)) { - return false; - } - return count($results) > 0; - } - -/** - * Recursively filters a data set. - * - * @param array $data Either an array to filter, or value when in callback - * @param callable $callback A function to filter the data with. Defaults to - * `static::_filter()` Which strips out all non-zero empty values. - * @return array Filtered array - * @link https://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::filter - */ - public static function filter(array $data, $callback = array('self', '_filter')) { - foreach ($data as $k => $v) { - if (is_array($v)) { - $data[$k] = static::filter($v, $callback); - } - } - return array_filter($data, $callback); - } - -/** - * Callback function for filtering. - * - * @param array $var Array to filter. - * @return bool - */ - protected static function _filter($var) { - if ($var === 0 || $var === 0.0 || $var === '0' || !empty($var)) { - return true; - } - return false; - } - -/** - * Collapses a multi-dimensional array into a single dimension, using a delimited array path for - * each array element's key, i.e. array(array('Foo' => array('Bar' => 'Far'))) becomes - * array('0.Foo.Bar' => 'Far').) - * - * @param array $data Array to flatten - * @param string $separator String used to separate array key elements in a path, defaults to '.' - * @return array - * @link https://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::flatten - */ - public static function flatten(array $data, $separator = '.') { - $result = array(); - $stack = array(); - $path = null; - - reset($data); - while (!empty($data)) { - $key = key($data); - $element = $data[$key]; - unset($data[$key]); - - if (is_array($element) && !empty($element)) { - if (!empty($data)) { - $stack[] = array($data, $path); - } - $data = $element; - reset($data); - $path .= $key . $separator; - } else { - $result[$path . $key] = $element; - } - - if (empty($data) && !empty($stack)) { - list($data, $path) = array_pop($stack); - reset($data); - } - } - return $result; - } - -/** - * Expands a flat array to a nested array. - * - * For example, unflattens an array that was collapsed with `Hash::flatten()` - * into a multi-dimensional array. So, `array('0.Foo.Bar' => 'Far')` becomes - * `array(array('Foo' => array('Bar' => 'Far')))`. - * - * @param array $data Flattened array - * @param string $separator The delimiter used - * @return array - * @link https://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::expand - */ - public static function expand($data, $separator = '.') { - $result = array(); - - $stack = array(); - - foreach ($data as $flat => $value) { - $keys = explode($separator, $flat); - $keys = array_reverse($keys); - $child = array( - $keys[0] => $value - ); - array_shift($keys); - foreach ($keys as $k) { - $child = array( - $k => $child - ); - } - - $stack[] = array($child, &$result); - - while (!empty($stack)) { - foreach ($stack as $curKey => &$curMerge) { - foreach ($curMerge[0] as $key => &$val) { - if (!empty($curMerge[1][$key]) && (array)$curMerge[1][$key] === $curMerge[1][$key] && (array)$val === $val) { - $stack[] = array(&$val, &$curMerge[1][$key]); - } elseif ((int)$key === $key && isset($curMerge[1][$key])) { - $curMerge[1][] = $val; - } else { - $curMerge[1][$key] = $val; - } - } - unset($stack[$curKey]); - } - unset($curMerge); - } - } - return $result; - } - -/** - * This function can be thought of as a hybrid between PHP's `array_merge` and `array_merge_recursive`. - * - * The difference between this method and the built-in ones, is that if an array key contains another array, then - * Hash::merge() will behave in a recursive fashion (unlike `array_merge`). But it will not act recursively for - * keys that contain scalar values (unlike `array_merge_recursive`). - * - * Note: This function will work with an unlimited amount of arguments and typecasts non-array parameters into arrays. - * - * @param array $data Array to be merged - * @param mixed $merge Array to merge with. The argument and all trailing arguments will be array cast when merged - * @return array Merged array - * @link https://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::merge - */ - public static function merge(array $data, $merge) { - $args = array_slice(func_get_args(), 1); - $return = $data; - - foreach ($args as &$curArg) { - $stack[] = array((array)$curArg, &$return); - } - unset($curArg); - - while (!empty($stack)) { - foreach ($stack as $curKey => &$curMerge) { - foreach ($curMerge[0] as $key => &$val) { - if (!empty($curMerge[1][$key]) && (array)$curMerge[1][$key] === $curMerge[1][$key] && (array)$val === $val) { - $stack[] = array(&$val, &$curMerge[1][$key]); - } elseif ((int)$key === $key && isset($curMerge[1][$key])) { - $curMerge[1][] = $val; - } else { - $curMerge[1][$key] = $val; - } - } - unset($stack[$curKey]); - } - unset($curMerge); - } - return $return; - } - -/** - * Checks to see if all the values in the array are numeric - * - * @param array $data The array to check. - * @return bool true if values are numeric, false otherwise - * @link https://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::numeric - */ - public static function numeric(array $data) { - if (empty($data)) { - return false; - } - return $data === array_filter($data, 'is_numeric'); - } - -/** - * Counts the dimensions of an array. - * Only considers the dimension of the first element in the array. - * - * If you have an un-even or heterogeneous array, consider using Hash::maxDimensions() - * to get the dimensions of the array. - * - * @param array $data Array to count dimensions on - * @return int The number of dimensions in $data - * @link https://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::dimensions - */ - public static function dimensions(array $data) { - if (empty($data)) { - return 0; - } - reset($data); - $depth = 1; - while ($elem = array_shift($data)) { - if (is_array($elem)) { - $depth += 1; - $data =& $elem; - } else { - break; - } - } - return $depth; - } - -/** - * Counts the dimensions of *all* array elements. Useful for finding the maximum - * number of dimensions in a mixed array. - * - * @param array $data Array to count dimensions on - * @return int The maximum number of dimensions in $data - * @link https://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::maxDimensions - */ - public static function maxDimensions($data) { - $depth = array(); - if (is_array($data) && reset($data) !== false) { - foreach ($data as $value) { - $depth[] = static::maxDimensions($value) + 1; - } - } - return empty($depth) ? 0 : max($depth); - } - -/** - * Map a callback across all elements in a set. - * Can be provided a path to only modify slices of the set. - * - * @param array $data The data to map over, and extract data out of. - * @param string $path The path to extract for mapping over. - * @param callable $function The function to call on each extracted value. - * @return array An array of the modified values. - * @link https://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::map - */ - public static function map(array $data, $path, $function) { - $values = (array)static::extract($data, $path); - return array_map($function, $values); - } - -/** - * Reduce a set of extracted values using `$function`. - * - * @param array $data The data to reduce. - * @param string $path The path to extract from $data. - * @param callable $function The function to call on each extracted value. - * @return mixed The reduced value. - * @link https://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::reduce - */ - public static function reduce(array $data, $path, $function) { - $values = (array)static::extract($data, $path); - return array_reduce($values, $function); - } - -/** - * Apply a callback to a set of extracted values using `$function`. - * The function will get the extracted values as the first argument. - * - * ### Example - * - * You can easily count the results of an extract using apply(). - * For example to count the comments on an Article: - * - * ``` - * $count = Hash::apply($data, 'Article.Comment.{n}', 'count'); - * ``` - * - * You could also use a function like `array_sum` to sum the results. - * - * ``` - * $total = Hash::apply($data, '{n}.Item.price', 'array_sum'); - * ``` - * - * @param array $data The data to reduce. - * @param string $path The path to extract from $data. - * @param callable $function The function to call on each extracted value. - * @return mixed The results of the applied method. - */ - public static function apply(array $data, $path, $function) { - $values = (array)static::extract($data, $path); - return call_user_func($function, $values); - } - -/** - * Sorts an array by any value, determined by a Hash-compatible path - * - * ### Sort directions - * - * - `asc` Sort ascending. - * - `desc` Sort descending. - * - * ### Sort types - * - * - `regular` For regular sorting (don't change types) - * - `numeric` Compare values numerically - * - `string` Compare values as strings - * - `locale` Compare items as strings, based on the current locale - * - `natural` Compare items as strings using "natural ordering" in a human friendly way. - * Will sort foo10 below foo2 as an example. Requires PHP 5.4 or greater or it will fallback to 'regular' - * - * To do case insensitive sorting, pass the type as an array as follows: - * - * ``` - * array('type' => 'regular', 'ignoreCase' => true) - * ``` - * - * When using the array form, `type` defaults to 'regular'. The `ignoreCase` option - * defaults to `false`. - * - * @param array $data An array of data to sort - * @param string $path A Hash-compatible path to the array value - * @param string $dir See directions above. Defaults to 'asc'. - * @param array|string $type See direction types above. Defaults to 'regular'. - * @return array Sorted array of data - * @link https://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::sort - */ - public static function sort(array $data, $path, $dir = 'asc', $type = 'regular') { - if (empty($data)) { - return array(); - } - $originalKeys = array_keys($data); - $numeric = is_numeric(implode('', $originalKeys)); - if ($numeric) { - $data = array_values($data); - } - $sortValues = static::extract($data, $path); - $dataCount = count($data); - - // Make sortValues match the data length, as some keys could be missing - // the sorted value path. - $missingData = count($sortValues) < $dataCount; - if ($missingData && $numeric) { - // Get the path without the leading '{n}.' - $itemPath = substr($path, 4); - foreach ($data as $key => $value) { - $sortValues[$key] = static::get($value, $itemPath); - } - } elseif ($missingData) { - $sortValues = array_pad($sortValues, $dataCount, null); - } - $result = static::_squash($sortValues); - $keys = static::extract($result, '{n}.id'); - $values = static::extract($result, '{n}.value'); - - $dir = strtolower($dir); - $ignoreCase = false; - - // $type can be overloaded for case insensitive sort - if (is_array($type)) { - $type += array('ignoreCase' => false, 'type' => 'regular'); - $ignoreCase = $type['ignoreCase']; - $type = $type['type']; - } - $type = strtolower($type); - - if ($type === 'natural' && version_compare(PHP_VERSION, '5.4.0', '<')) { - $type = 'regular'; - } - - if ($dir === 'asc') { - $dir = SORT_ASC; - } else { - $dir = SORT_DESC; - } - if ($type === 'numeric') { - $type = SORT_NUMERIC; - } elseif ($type === 'string') { - $type = SORT_STRING; - } elseif ($type === 'natural') { - $type = SORT_NATURAL; - } elseif ($type === 'locale') { - $type = SORT_LOCALE_STRING; - } else { - $type = SORT_REGULAR; - } - - if ($ignoreCase) { - $values = array_map('mb_strtolower', $values); - } - array_multisort($values, $dir, $type, $keys, $dir); - - $sorted = array(); - $keys = array_unique($keys); - - foreach ($keys as $k) { - if ($numeric) { - $sorted[] = $data[$k]; - continue; - } - if (isset($originalKeys[$k])) { - $sorted[$originalKeys[$k]] = $data[$originalKeys[$k]]; - } else { - $sorted[$k] = $data[$k]; - } - } - return $sorted; - } - -/** - * Helper method for sort() - * Squashes an array to a single hash so it can be sorted. - * - * @param array $data The data to squash. - * @param string $key The key for the data. - * @return array - */ - protected static function _squash($data, $key = null) { - $stack = array(); - foreach ($data as $k => $r) { - $id = $k; - if ($key !== null) { - $id = $key; - } - if (is_array($r) && !empty($r)) { - $stack = array_merge($stack, static::_squash($r, $id)); - } else { - $stack[] = array('id' => $id, 'value' => $r); - } - } - return $stack; - } - -/** - * Computes the difference between two complex arrays. - * This method differs from the built-in array_diff() in that it will preserve keys - * and work on multi-dimensional arrays. - * - * @param array $data First value - * @param array $compare Second value - * @return array Returns the key => value pairs that are not common in $data and $compare - * The expression for this function is ($data - $compare) + ($compare - ($data - $compare)) - * @link https://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::diff - */ - public static function diff(array $data, $compare) { - if (empty($data)) { - return (array)$compare; - } - if (empty($compare)) { - return (array)$data; - } - $intersection = array_intersect_key($data, $compare); - while (($key = key($intersection)) !== null) { - if ($data[$key] == $compare[$key]) { - unset($data[$key]); - unset($compare[$key]); - } - next($intersection); - } - return $data + $compare; - } - -/** - * Merges the difference between $data and $compare onto $data. - * - * @param array $data The data to append onto. - * @param array $compare The data to compare and append onto. - * @return array The merged array. - * @link https://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::mergeDiff - */ - public static function mergeDiff(array $data, $compare) { - if (empty($data) && !empty($compare)) { - return $compare; - } - if (empty($compare)) { - return $data; - } - foreach ($compare as $key => $value) { - if (!array_key_exists($key, $data)) { - $data[$key] = $value; - } elseif (is_array($value)) { - $data[$key] = static::mergeDiff($data[$key], $compare[$key]); - } - } - return $data; - } - -/** - * Normalizes an array, and converts it to a standard format. - * - * @param array $data List to normalize - * @param bool $assoc If true, $data will be converted to an associative array. - * @return array - * @link https://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::normalize - */ - public static function normalize(array $data, $assoc = true) { - $keys = array_keys($data); - $count = count($keys); - $numeric = true; - - if (!$assoc) { - for ($i = 0; $i < $count; $i++) { - if (!is_int($keys[$i])) { - $numeric = false; - break; - } - } - } - if (!$numeric || $assoc) { - $newList = array(); - for ($i = 0; $i < $count; $i++) { - if (is_int($keys[$i])) { - $newList[$data[$keys[$i]]] = null; - } else { - $newList[$keys[$i]] = $data[$keys[$i]]; - } - } - $data = $newList; - } - return $data; - } - -/** - * Takes in a flat array and returns a nested array - * - * ### Options: - * - * - `children` The key name to use in the resultset for children. - * - `idPath` The path to a key that identifies each entry. Should be - * compatible with Hash::extract(). Defaults to `{n}.$alias.id` - * - `parentPath` The path to a key that identifies the parent of each entry. - * Should be compatible with Hash::extract(). Defaults to `{n}.$alias.parent_id` - * - `root` The id of the desired top-most result. - * - * @param array $data The data to nest. - * @param array $options Options are: - * @return array of results, nested - * @see Hash::extract() - * @throws InvalidArgumentException When providing invalid data. - * @link https://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::nest - */ - public static function nest(array $data, $options = array()) { - if (!$data) { - return $data; - } - - $alias = key(current($data)); - $options += array( - 'idPath' => "{n}.$alias.id", - 'parentPath' => "{n}.$alias.parent_id", - 'children' => 'children', - 'root' => null - ); - - $return = $idMap = array(); - $ids = static::extract($data, $options['idPath']); - - $idKeys = explode('.', $options['idPath']); - array_shift($idKeys); - - $parentKeys = explode('.', $options['parentPath']); - array_shift($parentKeys); - - foreach ($data as $result) { - $result[$options['children']] = array(); - - $id = static::get($result, $idKeys); - $parentId = static::get($result, $parentKeys); - - if (isset($idMap[$id][$options['children']])) { - $idMap[$id] = array_merge($result, (array)$idMap[$id]); - } else { - $idMap[$id] = array_merge($result, array($options['children'] => array())); - } - if (!$parentId || !in_array($parentId, $ids)) { - $return[] =& $idMap[$id]; - } else { - $idMap[$parentId][$options['children']][] =& $idMap[$id]; - } - } - - if (!$return) { - throw new InvalidArgumentException(__d('cake_dev', - 'Invalid data array to nest.' - )); - } - - if ($options['root']) { - $root = $options['root']; - } else { - $root = static::get($return[0], $parentKeys); - } - - foreach ($return as $i => $result) { - $id = static::get($result, $idKeys); - $parentId = static::get($result, $parentKeys); - if ($id !== $root && $parentId != $root) { - unset($return[$i]); - } - } - return array_values($return); - } +class Hash +{ + + /** + * Insert $values into an array with the given $path. You can use + * `{n}` and `{s}` elements to insert $data multiple times. + * + * @param array $data The data to insert into. + * @param string $path The path to insert at. + * @param mixed $values The values to insert. + * @return array The data with $values inserted. + * @link https://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::insert + */ + public static function insert(array $data, $path, $values = null) + { + if (strpos($path, '[') === false) { + $tokens = explode('.', $path); + } else { + $tokens = CakeText::tokenize($path, '.', '[', ']'); + } + + if (strpos($path, '{') === false && strpos($path, '[') === false) { + return static::_simpleOp('insert', $data, $tokens, $values); + } + + $token = array_shift($tokens); + $nextPath = implode('.', $tokens); + + list($token, $conditions) = static::_splitConditions($token); + + foreach ($data as $k => $v) { + if (static::_matchToken($k, $token)) { + if (!$conditions || static::_matches($v, $conditions)) { + $data[$k] = $nextPath + ? static::insert($v, $nextPath, $values) + : array_merge($v, (array)$values); + } + } + } + return $data; + } + + /** + * Perform a simple insert/remove operation. + * + * @param string $op The operation to do. + * @param array $data The data to operate on. + * @param array $path The path to work on. + * @param mixed $values The values to insert when doing inserts. + * @return array data. + */ + protected static function _simpleOp($op, $data, $path, $values = null) + { + $_list =& $data; + + $count = count($path); + $last = $count - 1; + foreach ($path as $i => $key) { + if ($op === 'insert') { + if ($i === $last) { + $_list[$key] = $values; + return $data; + } + if (!isset($_list[$key])) { + $_list[$key] = []; + } + $_list =& $_list[$key]; + if (!is_array($_list)) { + $_list = []; + } + } else if ($op === 'remove') { + if ($i === $last) { + if (is_array($_list)) { + unset($_list[$key]); + } + return $data; + } + if (!isset($_list[$key])) { + return $data; + } + $_list =& $_list[$key]; + } + } + } + + /** + * Split token conditions + * + * @param string $token the token being splitted. + * @return array array(token, conditions) with token splitted + */ + protected static function _splitConditions($token) + { + $conditions = false; + $position = strpos($token, '['); + if ($position !== false) { + $conditions = substr($token, $position); + $token = substr($token, 0, $position); + } + + return [$token, $conditions]; + } + + /** + * Check a key against a token. + * + * @param string $key The key in the array being searched. + * @param string $token The token being matched. + * @return bool + */ + protected static function _matchToken($key, $token) + { + switch ($token) { + case '{n}': + return is_numeric($key); + case '{s}': + return is_string($key); + case '{*}': + return true; + default: + return is_numeric($token) ? ($key == $token) : $key === $token; + } + } + + /** + * Checks whether or not $data matches the attribute patterns + * + * @param array $data Array of data to match. + * @param string $selector The patterns to match. + * @return bool Fitness of expression. + */ + protected static function _matches(array $data, $selector) + { + preg_match_all( + '/(\[ (?P[^=>[><]) \s* (?P(?:\/.*?\/ | [^\]]+)) )? \])/x', + $selector, + $conditions, + PREG_SET_ORDER + ); + + foreach ($conditions as $cond) { + $attr = $cond['attr']; + $op = isset($cond['op']) ? $cond['op'] : null; + $val = isset($cond['val']) ? $cond['val'] : null; + + // Presence test. + if (empty($op) && empty($val) && !isset($data[$attr])) { + return false; + } + + // Empty attribute = fail. + if (!(isset($data[$attr]) || array_key_exists($attr, $data))) { + return false; + } + + $prop = null; + if (isset($data[$attr])) { + $prop = $data[$attr]; + } + $isBool = is_bool($prop); + if ($isBool && is_numeric($val)) { + $prop = $prop ? '1' : '0'; + } else if ($isBool) { + $prop = $prop ? 'true' : 'false'; + } + + // Pattern matches and other operators. + if ($op === '=' && $val && $val[0] === '/') { + if (!preg_match($val, $prop)) { + return false; + } + } else if (($op === '=' && $prop != $val) || + ($op === '!=' && $prop == $val) || + ($op === '>' && $prop <= $val) || + ($op === '<' && $prop >= $val) || + ($op === '>=' && $prop < $val) || + ($op === '<=' && $prop > $val) + ) { + return false; + } + + } + return true; + } + + /** + * Remove data matching $path from the $data array. + * You can use `{n}` and `{s}` to remove multiple elements + * from $data. + * + * @param array $data The data to operate on + * @param string $path A path expression to use to remove. + * @return array The modified array. + * @link https://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::remove + */ + public static function remove(array $data, $path) + { + if (strpos($path, '[') === false) { + $tokens = explode('.', $path); + } else { + $tokens = CakeText::tokenize($path, '.', '[', ']'); + } + + if (strpos($path, '{') === false && strpos($path, '[') === false) { + return static::_simpleOp('remove', $data, $tokens); + } + + $token = array_shift($tokens); + $nextPath = implode('.', $tokens); + + list($token, $conditions) = static::_splitConditions($token); + + foreach ($data as $k => $v) { + $match = static::_matchToken($k, $token); + if ($match && is_array($v)) { + if ($conditions) { + if (static::_matches($v, $conditions)) { + if ($nextPath !== '') { + $data[$k] = static::remove($v, $nextPath); + } else { + unset($data[$k]); + } + } + } else { + $data[$k] = static::remove($v, $nextPath); + } + if (empty($data[$k])) { + unset($data[$k]); + } + } else if ($match && $nextPath === '') { + unset($data[$k]); + } + } + return $data; + } + + /** + * Creates an associative array using `$keyPath` as the path to build its keys, and optionally + * `$valuePath` as path to get the values. If `$valuePath` is not specified, all values will be initialized + * to null (useful for Hash::merge). You can optionally group the values by what is obtained when + * following the path specified in `$groupPath`. + * + * @param array $data Array from where to extract keys and values + * @param array|string $keyPath A dot-separated string or array for formatting rules. + * @param array|string $valuePath A dot-separated string or array for formatting rules. + * @param string $groupPath A dot-separated string. + * @return array Combined array + * @throws CakeException CakeException When keys and values count is unequal. + * @link https://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::combine + */ + public static function combine(array $data, $keyPath, $valuePath = null, $groupPath = null) + { + if (empty($data)) { + return []; + } + + if (is_array($keyPath)) { + $format = array_shift($keyPath); + $keys = static::format($data, $keyPath, $format); + } else { + $keys = static::extract($data, $keyPath); + } + if (empty($keys)) { + return []; + } + + if (!empty($valuePath) && is_array($valuePath)) { + $format = array_shift($valuePath); + $vals = static::format($data, $valuePath, $format); + } else if (!empty($valuePath)) { + $vals = static::extract($data, $valuePath); + } + if (empty($vals)) { + $vals = array_fill(0, count($keys), null); + } + + if (count($keys) !== count($vals)) { + throw new CakeException(__d( + 'cake_dev', + 'Hash::combine() needs an equal number of keys + values.' + )); + } + + if ($groupPath !== null) { + $group = static::extract($data, $groupPath); + if (!empty($group)) { + $c = count($keys); + for ($i = 0; $i < $c; $i++) { + if (!isset($group[$i])) { + $group[$i] = 0; + } + if (!isset($out[$group[$i]])) { + $out[$group[$i]] = []; + } + $out[$group[$i]][$keys[$i]] = $vals[$i]; + } + return $out; + } + } + if (empty($vals)) { + return []; + } + return array_combine($keys, $vals); + } + + /** + * Returns a formatted series of values extracted from `$data`, using + * `$format` as the format and `$paths` as the values to extract. + * + * Usage: + * + * ``` + * $result = Hash::format($users, array('{n}.User.id', '{n}.User.name'), '%s : %s'); + * ``` + * + * The `$format` string can use any format options that `vsprintf()` and `sprintf()` do. + * + * @param array $data Source array from which to extract the data + * @param array $paths An array containing one or more Hash::extract()-style key paths + * @param string $format Format string into which values will be inserted, see sprintf() + * @return array An array of strings extracted from `$path` and formatted with `$format` + * @link https://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::format + * @see sprintf() + * @see Hash::extract() + * @link https://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::format + */ + public static function format(array $data, array $paths, $format) + { + $extracted = []; + $count = count($paths); + + if (!$count) { + return null; + } + + for ($i = 0; $i < $count; $i++) { + $extracted[] = static::extract($data, $paths[$i]); + } + $out = []; + $data = $extracted; + $count = count($data[0]); + + $countTwo = count($data); + for ($j = 0; $j < $count; $j++) { + $args = []; + for ($i = 0; $i < $countTwo; $i++) { + if (array_key_exists($j, $data[$i])) { + $args[] = $data[$i][$j]; + } + } + $out[] = vsprintf($format, $args); + } + return $out; + } + + /** + * Gets the values from an array matching the $path expression. + * The path expression is a dot separated expression, that can contain a set + * of patterns and expressions: + * + * - `{n}` Matches any numeric key, or integer. + * - `{s}` Matches any string key. + * - `{*}` Matches any value. + * - `Foo` Matches any key with the exact same value. + * + * There are a number of attribute operators: + * + * - `=`, `!=` Equality. + * - `>`, `<`, `>=`, `<=` Value comparison. + * - `=/.../` Regular expression pattern match. + * + * Given a set of User array data, from a `$User->find('all')` call: + * + * - `1.User.name` Get the name of the user at index 1. + * - `{n}.User.name` Get the name of every user in the set of users. + * - `{n}.User[id].name` Get the name of every user with an id key. + * - `{n}.User[id>=2].name` Get the name of every user with an id key greater than or equal to 2. + * - `{n}.User[username=/^paul/]` Get User elements with username matching `^paul`. + * + * @param array $data The data to extract from. + * @param string $path The path to extract. + * @return array An array of the extracted values. Returns an empty array + * if there are no matches. + * @link https://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::extract + */ + public static function extract(array $data, $path) + { + if (empty($path)) { + return $data; + } + + // Simple paths. + if (!preg_match('/[{\[]/', $path)) { + return (array)static::get($data, $path); + } + + if (strpos($path, '[') === false) { + $tokens = explode('.', $path); + } else { + $tokens = CakeText::tokenize($path, '.', '[', ']'); + } + + $_key = '__set_item__'; + + $context = [$_key => [$data]]; + + foreach ($tokens as $token) { + $next = []; + + list($token, $conditions) = static::_splitConditions($token); + + foreach ($context[$_key] as $item) { + foreach ((array)$item as $k => $v) { + if (static::_matchToken($k, $token)) { + $next[] = $v; + } + } + } + + // Filter for attributes. + if ($conditions) { + $filter = []; + foreach ($next as $item) { + if (is_array($item) && static::_matches($item, $conditions)) { + $filter[] = $item; + } + } + $next = $filter; + } + $context = [$_key => $next]; + + } + return $context[$_key]; + } + + /** + * Get a single value specified by $path out of $data. + * Does not support the full dot notation feature set, + * but is faster for simple read operations. + * + * @param array $data Array of data to operate on. + * @param string|array $path The path being searched for. Either a dot + * separated string, or an array of path segments. + * @param mixed $default The return value when the path does not exist + * @return mixed The value fetched from the array, or null. + * @throws InvalidArgumentException + * @link https://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::get + */ + public static function get(array $data, $path, $default = null) + { + if (empty($data) || $path === null) { + return $default; + } + if (is_string($path) || is_numeric($path)) { + $parts = explode('.', $path); + } else if (is_bool($path) || $path === null) { + $parts = [$path]; + } else { + if (!is_array($path)) { + throw new InvalidArgumentException(__d('cake_dev', + 'Invalid path parameter: %s, should be dot separated path or array.', + var_export($path, true) + )); + } + $parts = $path; + } + + foreach ($parts as $key) { + if (is_array($data) && isset($data[$key])) { + $data =& $data[$key]; + } else { + return $default; + } + } + + return $data; + } + + /** + * Determines if one array contains the exact keys and values of another. + * + * @param array $data The data to search through. + * @param array $needle The values to file in $data + * @return bool true if $data contains $needle, false otherwise + * @link https://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::contains + */ + public static function contains(array $data, array $needle) + { + if (empty($data) || empty($needle)) { + return false; + } + $stack = []; + + while (!empty($needle)) { + $key = key($needle); + $val = $needle[$key]; + unset($needle[$key]); + + if (array_key_exists($key, $data) && is_array($val)) { + $next = $data[$key]; + unset($data[$key]); + + if (!empty($val)) { + $stack[] = [$val, $next]; + } + } else if (!array_key_exists($key, $data) || $data[$key] != $val) { + return false; + } + + if (empty($needle) && !empty($stack)) { + list($needle, $data) = array_pop($stack); + } + } + return true; + } + + /** + * Test whether or not a given path exists in $data. + * This method uses the same path syntax as Hash::extract() + * + * Checking for paths that could target more than one element will + * make sure that at least one matching element exists. + * + * @param array $data The data to check. + * @param string $path The path to check for. + * @return bool Existence of path. + * @see Hash::extract() + * @link https://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::check + */ + public static function check(array $data, $path) + { + $results = static::extract($data, $path); + if (!is_array($results)) { + return false; + } + return count($results) > 0; + } + + /** + * Recursively filters a data set. + * + * @param array $data Either an array to filter, or value when in callback + * @param callable $callback A function to filter the data with. Defaults to + * `static::_filter()` Which strips out all non-zero empty values. + * @return array Filtered array + * @link https://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::filter + */ + public static function filter(array $data, $callback = ['self', '_filter']) + { + foreach ($data as $k => $v) { + if (is_array($v)) { + $data[$k] = static::filter($v, $callback); + } + } + return array_filter($data, $callback); + } + + /** + * Collapses a multi-dimensional array into a single dimension, using a delimited array path for + * each array element's key, i.e. array(array('Foo' => array('Bar' => 'Far'))) becomes + * array('0.Foo.Bar' => 'Far').) + * + * @param array $data Array to flatten + * @param string $separator String used to separate array key elements in a path, defaults to '.' + * @return array + * @link https://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::flatten + */ + public static function flatten(array $data, $separator = '.') + { + $result = []; + $stack = []; + $path = null; + + reset($data); + while (!empty($data)) { + $key = key($data); + $element = $data[$key]; + unset($data[$key]); + + if (is_array($element) && !empty($element)) { + if (!empty($data)) { + $stack[] = [$data, $path]; + } + $data = $element; + reset($data); + $path .= $key . $separator; + } else { + $result[$path . $key] = $element; + } + + if (empty($data) && !empty($stack)) { + list($data, $path) = array_pop($stack); + reset($data); + } + } + return $result; + } + + /** + * Expands a flat array to a nested array. + * + * For example, unflattens an array that was collapsed with `Hash::flatten()` + * into a multi-dimensional array. So, `array('0.Foo.Bar' => 'Far')` becomes + * `array(array('Foo' => array('Bar' => 'Far')))`. + * + * @param array $data Flattened array + * @param string $separator The delimiter used + * @return array + * @link https://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::expand + */ + public static function expand($data, $separator = '.') + { + $result = []; + + $stack = []; + + foreach ($data as $flat => $value) { + $keys = explode($separator, $flat); + $keys = array_reverse($keys); + $child = [ + $keys[0] => $value + ]; + array_shift($keys); + foreach ($keys as $k) { + $child = [ + $k => $child + ]; + } + + $stack[] = [$child, &$result]; + + while (!empty($stack)) { + foreach ($stack as $curKey => &$curMerge) { + foreach ($curMerge[0] as $key => &$val) { + if (!empty($curMerge[1][$key]) && (array)$curMerge[1][$key] === $curMerge[1][$key] && (array)$val === $val) { + $stack[] = [&$val, &$curMerge[1][$key]]; + } else if ((int)$key === $key && isset($curMerge[1][$key])) { + $curMerge[1][] = $val; + } else { + $curMerge[1][$key] = $val; + } + } + unset($stack[$curKey]); + } + unset($curMerge); + } + } + return $result; + } + + /** + * This function can be thought of as a hybrid between PHP's `array_merge` and `array_merge_recursive`. + * + * The difference between this method and the built-in ones, is that if an array key contains another array, then + * Hash::merge() will behave in a recursive fashion (unlike `array_merge`). But it will not act recursively for + * keys that contain scalar values (unlike `array_merge_recursive`). + * + * Note: This function will work with an unlimited amount of arguments and typecasts non-array parameters into arrays. + * + * @param array $data Array to be merged + * @param mixed $merge Array to merge with. The argument and all trailing arguments will be array cast when merged + * @return array Merged array + * @link https://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::merge + */ + public static function merge(array $data, $merge) + { + $args = array_slice(func_get_args(), 1); + $return = $data; + + foreach ($args as &$curArg) { + $stack[] = [(array)$curArg, &$return]; + } + unset($curArg); + + while (!empty($stack)) { + foreach ($stack as $curKey => &$curMerge) { + foreach ($curMerge[0] as $key => &$val) { + if (!empty($curMerge[1][$key]) && (array)$curMerge[1][$key] === $curMerge[1][$key] && (array)$val === $val) { + $stack[] = [&$val, &$curMerge[1][$key]]; + } else if ((int)$key === $key && isset($curMerge[1][$key])) { + $curMerge[1][] = $val; + } else { + $curMerge[1][$key] = $val; + } + } + unset($stack[$curKey]); + } + unset($curMerge); + } + return $return; + } + + /** + * Checks to see if all the values in the array are numeric + * + * @param array $data The array to check. + * @return bool true if values are numeric, false otherwise + * @link https://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::numeric + */ + public static function numeric(array $data) + { + if (empty($data)) { + return false; + } + return $data === array_filter($data, 'is_numeric'); + } + + /** + * Counts the dimensions of an array. + * Only considers the dimension of the first element in the array. + * + * If you have an un-even or heterogeneous array, consider using Hash::maxDimensions() + * to get the dimensions of the array. + * + * @param array $data Array to count dimensions on + * @return int The number of dimensions in $data + * @link https://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::dimensions + */ + public static function dimensions(array $data) + { + if (empty($data)) { + return 0; + } + reset($data); + $depth = 1; + while ($elem = array_shift($data)) { + if (is_array($elem)) { + $depth += 1; + $data =& $elem; + } else { + break; + } + } + return $depth; + } + + /** + * Counts the dimensions of *all* array elements. Useful for finding the maximum + * number of dimensions in a mixed array. + * + * @param array $data Array to count dimensions on + * @return int The maximum number of dimensions in $data + * @link https://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::maxDimensions + */ + public static function maxDimensions($data) + { + $depth = []; + if (is_array($data) && reset($data) !== false) { + foreach ($data as $value) { + $depth[] = static::maxDimensions($value) + 1; + } + } + return empty($depth) ? 0 : max($depth); + } + + /** + * Map a callback across all elements in a set. + * Can be provided a path to only modify slices of the set. + * + * @param array $data The data to map over, and extract data out of. + * @param string $path The path to extract for mapping over. + * @param callable $function The function to call on each extracted value. + * @return array An array of the modified values. + * @link https://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::map + */ + public static function map(array $data, $path, $function) + { + $values = (array)static::extract($data, $path); + return array_map($function, $values); + } + + /** + * Reduce a set of extracted values using `$function`. + * + * @param array $data The data to reduce. + * @param string $path The path to extract from $data. + * @param callable $function The function to call on each extracted value. + * @return mixed The reduced value. + * @link https://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::reduce + */ + public static function reduce(array $data, $path, $function) + { + $values = (array)static::extract($data, $path); + return array_reduce($values, $function); + } + + /** + * Apply a callback to a set of extracted values using `$function`. + * The function will get the extracted values as the first argument. + * + * ### Example + * + * You can easily count the results of an extract using apply(). + * For example to count the comments on an Article: + * + * ``` + * $count = Hash::apply($data, 'Article.Comment.{n}', 'count'); + * ``` + * + * You could also use a function like `array_sum` to sum the results. + * + * ``` + * $total = Hash::apply($data, '{n}.Item.price', 'array_sum'); + * ``` + * + * @param array $data The data to reduce. + * @param string $path The path to extract from $data. + * @param callable $function The function to call on each extracted value. + * @return mixed The results of the applied method. + */ + public static function apply(array $data, $path, $function) + { + $values = (array)static::extract($data, $path); + return call_user_func($function, $values); + } + + /** + * Sorts an array by any value, determined by a Hash-compatible path + * + * ### Sort directions + * + * - `asc` Sort ascending. + * - `desc` Sort descending. + * + * ### Sort types + * + * - `regular` For regular sorting (don't change types) + * - `numeric` Compare values numerically + * - `string` Compare values as strings + * - `locale` Compare items as strings, based on the current locale + * - `natural` Compare items as strings using "natural ordering" in a human friendly way. + * Will sort foo10 below foo2 as an example. Requires PHP 5.4 or greater or it will fallback to 'regular' + * + * To do case insensitive sorting, pass the type as an array as follows: + * + * ``` + * array('type' => 'regular', 'ignoreCase' => true) + * ``` + * + * When using the array form, `type` defaults to 'regular'. The `ignoreCase` option + * defaults to `false`. + * + * @param array $data An array of data to sort + * @param string $path A Hash-compatible path to the array value + * @param string $dir See directions above. Defaults to 'asc'. + * @param array|string $type See direction types above. Defaults to 'regular'. + * @return array Sorted array of data + * @link https://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::sort + */ + public static function sort(array $data, $path, $dir = 'asc', $type = 'regular') + { + if (empty($data)) { + return []; + } + $originalKeys = array_keys($data); + $numeric = is_numeric(implode('', $originalKeys)); + if ($numeric) { + $data = array_values($data); + } + $sortValues = static::extract($data, $path); + $dataCount = count($data); + + // Make sortValues match the data length, as some keys could be missing + // the sorted value path. + $missingData = count($sortValues) < $dataCount; + if ($missingData && $numeric) { + // Get the path without the leading '{n}.' + $itemPath = substr($path, 4); + foreach ($data as $key => $value) { + $sortValues[$key] = static::get($value, $itemPath); + } + } else if ($missingData) { + $sortValues = array_pad($sortValues, $dataCount, null); + } + $result = static::_squash($sortValues); + $keys = static::extract($result, '{n}.id'); + $values = static::extract($result, '{n}.value'); + + $dir = strtolower($dir); + $ignoreCase = false; + + // $type can be overloaded for case insensitive sort + if (is_array($type)) { + $type += ['ignoreCase' => false, 'type' => 'regular']; + $ignoreCase = $type['ignoreCase']; + $type = $type['type']; + } + $type = strtolower($type); + + if ($type === 'natural' && version_compare(PHP_VERSION, '5.4.0', '<')) { + $type = 'regular'; + } + + if ($dir === 'asc') { + $dir = SORT_ASC; + } else { + $dir = SORT_DESC; + } + if ($type === 'numeric') { + $type = SORT_NUMERIC; + } else if ($type === 'string') { + $type = SORT_STRING; + } else if ($type === 'natural') { + $type = SORT_NATURAL; + } else if ($type === 'locale') { + $type = SORT_LOCALE_STRING; + } else { + $type = SORT_REGULAR; + } + + if ($ignoreCase) { + $values = array_map('mb_strtolower', $values); + } + array_multisort($values, $dir, $type, $keys, $dir); + + $sorted = []; + $keys = array_unique($keys); + + foreach ($keys as $k) { + if ($numeric) { + $sorted[] = $data[$k]; + continue; + } + if (isset($originalKeys[$k])) { + $sorted[$originalKeys[$k]] = $data[$originalKeys[$k]]; + } else { + $sorted[$k] = $data[$k]; + } + } + return $sorted; + } + + /** + * Helper method for sort() + * Squashes an array to a single hash so it can be sorted. + * + * @param array $data The data to squash. + * @param string $key The key for the data. + * @return array + */ + protected static function _squash($data, $key = null) + { + $stack = []; + foreach ($data as $k => $r) { + $id = $k; + if ($key !== null) { + $id = $key; + } + if (is_array($r) && !empty($r)) { + $stack = array_merge($stack, static::_squash($r, $id)); + } else { + $stack[] = ['id' => $id, 'value' => $r]; + } + } + return $stack; + } + + /** + * Computes the difference between two complex arrays. + * This method differs from the built-in array_diff() in that it will preserve keys + * and work on multi-dimensional arrays. + * + * @param array $data First value + * @param array $compare Second value + * @return array Returns the key => value pairs that are not common in $data and $compare + * The expression for this function is ($data - $compare) + ($compare - ($data - $compare)) + * @link https://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::diff + */ + public static function diff(array $data, $compare) + { + if (empty($data)) { + return (array)$compare; + } + if (empty($compare)) { + return (array)$data; + } + $intersection = array_intersect_key($data, $compare); + while (($key = key($intersection)) !== null) { + if ($data[$key] == $compare[$key]) { + unset($data[$key]); + unset($compare[$key]); + } + next($intersection); + } + return $data + $compare; + } + + /** + * Merges the difference between $data and $compare onto $data. + * + * @param array $data The data to append onto. + * @param array $compare The data to compare and append onto. + * @return array The merged array. + * @link https://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::mergeDiff + */ + public static function mergeDiff(array $data, $compare) + { + if (empty($data) && !empty($compare)) { + return $compare; + } + if (empty($compare)) { + return $data; + } + foreach ($compare as $key => $value) { + if (!array_key_exists($key, $data)) { + $data[$key] = $value; + } else if (is_array($value)) { + $data[$key] = static::mergeDiff($data[$key], $compare[$key]); + } + } + return $data; + } + + /** + * Normalizes an array, and converts it to a standard format. + * + * @param array $data List to normalize + * @param bool $assoc If true, $data will be converted to an associative array. + * @return array + * @link https://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::normalize + */ + public static function normalize(array $data, $assoc = true) + { + $keys = array_keys($data); + $count = count($keys); + $numeric = true; + + if (!$assoc) { + for ($i = 0; $i < $count; $i++) { + if (!is_int($keys[$i])) { + $numeric = false; + break; + } + } + } + if (!$numeric || $assoc) { + $newList = []; + for ($i = 0; $i < $count; $i++) { + if (is_int($keys[$i])) { + $newList[$data[$keys[$i]]] = null; + } else { + $newList[$keys[$i]] = $data[$keys[$i]]; + } + } + $data = $newList; + } + return $data; + } + + /** + * Takes in a flat array and returns a nested array + * + * ### Options: + * + * - `children` The key name to use in the resultset for children. + * - `idPath` The path to a key that identifies each entry. Should be + * compatible with Hash::extract(). Defaults to `{n}.$alias.id` + * - `parentPath` The path to a key that identifies the parent of each entry. + * Should be compatible with Hash::extract(). Defaults to `{n}.$alias.parent_id` + * - `root` The id of the desired top-most result. + * + * @param array $data The data to nest. + * @param array $options Options are: + * @return array of results, nested + * @throws InvalidArgumentException When providing invalid data. + * @see Hash::extract() + * @link https://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::nest + */ + public static function nest(array $data, $options = []) + { + if (!$data) { + return $data; + } + + $alias = key(current($data)); + $options += [ + 'idPath' => "{n}.$alias.id", + 'parentPath' => "{n}.$alias.parent_id", + 'children' => 'children', + 'root' => null + ]; + + $return = $idMap = []; + $ids = static::extract($data, $options['idPath']); + + $idKeys = explode('.', $options['idPath']); + array_shift($idKeys); + + $parentKeys = explode('.', $options['parentPath']); + array_shift($parentKeys); + + foreach ($data as $result) { + $result[$options['children']] = []; + + $id = static::get($result, $idKeys); + $parentId = static::get($result, $parentKeys); + + if (isset($idMap[$id][$options['children']])) { + $idMap[$id] = array_merge($result, (array)$idMap[$id]); + } else { + $idMap[$id] = array_merge($result, [$options['children'] => []]); + } + if (!$parentId || !in_array($parentId, $ids)) { + $return[] =& $idMap[$id]; + } else { + $idMap[$parentId][$options['children']][] =& $idMap[$id]; + } + } + + if (!$return) { + throw new InvalidArgumentException(__d('cake_dev', + 'Invalid data array to nest.' + )); + } + + if ($options['root']) { + $root = $options['root']; + } else { + $root = static::get($return[0], $parentKeys); + } + + foreach ($return as $i => $result) { + $id = static::get($result, $idKeys); + $parentId = static::get($result, $parentKeys); + if ($id !== $root && $parentId != $root) { + unset($return[$i]); + } + } + return array_values($return); + } + + /** + * Callback function for filtering. + * + * @param array $var Array to filter. + * @return bool + */ + protected static function _filter($var) + { + if ($var === 0 || $var === 0.0 || $var === '0' || !empty($var)) { + return true; + } + return false; + } } diff --git a/lib/Cake/Utility/Inflector.php b/lib/Cake/Utility/Inflector.php index 80c5c09a..8275ffd3 100755 --- a/lib/Cake/Utility/Inflector.php +++ b/lib/Cake/Utility/Inflector.php @@ -23,561 +23,574 @@ * @package Cake.Utility * @link https://book.cakephp.org/2.0/en/core-utility-libraries/inflector.html */ -class Inflector { - -/** - * Plural inflector rules - * - * @var array - */ - protected static $_plural = array( - 'rules' => array( - '/(s)tatus$/i' => '\1tatuses', - '/(quiz)$/i' => '\1zes', - '/^(ox)$/i' => '\1\2en', - '/([m|l])ouse$/i' => '\1ice', - '/(matr|vert|ind)(ix|ex)$/i' => '\1ices', - '/(x|ch|ss|sh)$/i' => '\1es', - '/([^aeiouy]|qu)y$/i' => '\1ies', - '/(hive)$/i' => '\1s', - '/(?:([^f])fe|([lre])f)$/i' => '\1\2ves', - '/sis$/i' => 'ses', - '/([ti])um$/i' => '\1a', - '/(p)erson$/i' => '\1eople', - '/(? '\1en', - '/(c)hild$/i' => '\1hildren', - '/(buffal|tomat)o$/i' => '\1\2oes', - '/(alumn|bacill|cact|foc|fung|nucle|radi|stimul|syllab|termin)us$/i' => '\1i', - '/us$/i' => 'uses', - '/(alias)$/i' => '\1es', - '/(ax|cris|test)is$/i' => '\1es', - '/s$/' => 's', - '/^$/' => '', - '/$/' => 's', - ), - 'uninflected' => array( - '.*[nrlm]ese', - '.*data', - '.*deer', - '.*fish', - '.*measles', - '.*ois', - '.*pox', - '.*sheep', - 'people', - 'feedback', - 'stadia' - ), - 'irregular' => array( - 'atlas' => 'atlases', - 'beef' => 'beefs', - 'brief' => 'briefs', - 'brother' => 'brothers', - 'cafe' => 'cafes', - 'child' => 'children', - 'cookie' => 'cookies', - 'corpus' => 'corpuses', - 'cow' => 'cows', - 'criterion' => 'criteria', - 'ganglion' => 'ganglions', - 'genie' => 'genies', - 'genus' => 'genera', - 'graffito' => 'graffiti', - 'hoof' => 'hoofs', - 'loaf' => 'loaves', - 'man' => 'men', - 'money' => 'monies', - 'mongoose' => 'mongooses', - 'move' => 'moves', - 'mythos' => 'mythoi', - 'niche' => 'niches', - 'numen' => 'numina', - 'occiput' => 'occiputs', - 'octopus' => 'octopuses', - 'opus' => 'opuses', - 'ox' => 'oxen', - 'penis' => 'penises', - 'person' => 'people', - 'sex' => 'sexes', - 'soliloquy' => 'soliloquies', - 'testis' => 'testes', - 'trilby' => 'trilbys', - 'turf' => 'turfs', - 'potato' => 'potatoes', - 'hero' => 'heroes', - 'tooth' => 'teeth', - 'goose' => 'geese', - 'foot' => 'feet', - 'sieve' => 'sieves' - ) - ); - -/** - * Singular inflector rules - * - * @var array - */ - protected static $_singular = array( - 'rules' => array( - '/(s)tatuses$/i' => '\1\2tatus', - '/^(.*)(menu)s$/i' => '\1\2', - '/(quiz)zes$/i' => '\\1', - '/(matr)ices$/i' => '\1ix', - '/(vert|ind)ices$/i' => '\1ex', - '/^(ox)en/i' => '\1', - '/(alias)(es)*$/i' => '\1', - '/(alumn|bacill|cact|foc|fung|nucle|radi|stimul|syllab|termin|viri?)i$/i' => '\1us', - '/([ftw]ax)es/i' => '\1', - '/(cris|ax|test)es$/i' => '\1is', - '/(shoe)s$/i' => '\1', - '/(o)es$/i' => '\1', - '/ouses$/' => 'ouse', - '/([^a])uses$/' => '\1us', - '/([m|l])ice$/i' => '\1ouse', - '/(x|ch|ss|sh)es$/i' => '\1', - '/(m)ovies$/i' => '\1\2ovie', - '/(s)eries$/i' => '\1\2eries', - '/([^aeiouy]|qu)ies$/i' => '\1y', - '/(tive)s$/i' => '\1', - '/(hive)s$/i' => '\1', - '/(drive)s$/i' => '\1', - '/([le])ves$/i' => '\1f', - '/([^rfoa])ves$/i' => '\1fe', - '/(^analy)ses$/i' => '\1sis', - '/(analy|diagno|^ba|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$/i' => '\1\2sis', - '/([ti])a$/i' => '\1um', - '/(p)eople$/i' => '\1\2erson', - '/(m)en$/i' => '\1an', - '/(c)hildren$/i' => '\1\2hild', - '/(n)ews$/i' => '\1\2ews', - '/eaus$/' => 'eau', - '/^(.*us)$/' => '\\1', - '/s$/i' => '' - ), - 'uninflected' => array( - '.*data', - '.*[nrlm]ese', '.*deer', '.*fish', '.*measles', '.*ois', '.*pox', '.*sheep', '.*ss', 'feedback' - ), - 'irregular' => array( - 'foes' => 'foe', - ) - ); - -/** - * Words that should not be inflected - * - * @var array - */ - protected static $_uninflected = array( - 'Amoyese', 'bison', 'Borghese', 'bream', 'breeches', 'britches', 'buffalo', 'cantus', - 'carp', 'chassis', 'clippers', 'cod', 'coitus', 'Congoese', 'contretemps', 'corps', - 'debris', 'diabetes', 'djinn', 'eland', 'elk', 'equipment', 'Faroese', 'flounder', - 'Foochowese', 'gallows', 'Genevese', 'Genoese', 'Gilbertese', 'graffiti', - 'headquarters', 'herpes', 'hijinks', 'Hottentotese', 'information', 'innings', - 'jackanapes', 'Kiplingese', 'Kongoese', 'Lucchese', 'mackerel', 'Maltese', '.*?media', - 'mews', 'moose', 'mumps', 'Nankingese', 'news', 'nexus', 'Niasese', - 'Pekingese', 'Piedmontese', 'pincers', 'Pistoiese', 'pliers', 'Portuguese', - 'proceedings', 'rabies', 'research', 'rice', 'rhinoceros', 'salmon', 'Sarawakese', 'scissors', - 'sea[- ]bass', 'series', 'Shavese', 'shears', 'siemens', 'species', 'swine', 'testes', - 'trousers', 'trout', 'tuna', 'Vermontese', 'Wenchowese', 'whiting', 'wildebeest', - 'Yengeese' - ); - -/** - * Default map of accented and special characters to ASCII characters - * - * @var array - */ - protected static $_transliteration = array( - '/À|Á|Â|Ã|Å|Ǻ|Ā|Ă|Ą|Ǎ/' => 'A', - '/Æ|Ǽ/' => 'AE', - '/Ä/' => 'Ae', - '/Ç|Ć|Ĉ|Ċ|Č/' => 'C', - '/Ð|Ď|Đ/' => 'D', - '/È|É|Ê|Ë|Ē|Ĕ|Ė|Ę|Ě/' => 'E', - '/Ĝ|Ğ|Ġ|Ģ|Ґ/' => 'G', - '/Ĥ|Ħ/' => 'H', - '/Ì|Í|Î|Ï|Ĩ|Ī|Ĭ|Ǐ|Į|İ|І/' => 'I', - '/IJ/' => 'IJ', - '/Ĵ/' => 'J', - '/Ķ/' => 'K', - '/Ĺ|Ļ|Ľ|Ŀ|Ł/' => 'L', - '/Ñ|Ń|Ņ|Ň/' => 'N', - '/Ò|Ó|Ô|Õ|Ō|Ŏ|Ǒ|Ő|Ơ|Ø|Ǿ/' => 'O', - '/Œ/' => 'OE', - '/Ö/' => 'Oe', - '/Ŕ|Ŗ|Ř/' => 'R', - '/Ś|Ŝ|Ş|Ș|Š/' => 'S', - '/ẞ/' => 'SS', - '/Ţ|Ț|Ť|Ŧ/' => 'T', - '/Þ/' => 'TH', - '/Ù|Ú|Û|Ũ|Ū|Ŭ|Ů|Ű|Ų|Ư|Ǔ|Ǖ|Ǘ|Ǚ|Ǜ/' => 'U', - '/Ü/' => 'Ue', - '/Ŵ/' => 'W', - '/Ý|Ÿ|Ŷ/' => 'Y', - '/Є/' => 'Ye', - '/Ї/' => 'Yi', - '/Ź|Ż|Ž/' => 'Z', - '/à|á|â|ã|å|ǻ|ā|ă|ą|ǎ|ª/' => 'a', - '/ä|æ|ǽ/' => 'ae', - '/ç|ć|ĉ|ċ|č/' => 'c', - '/ð|ď|đ/' => 'd', - '/è|é|ê|ë|ē|ĕ|ė|ę|ě/' => 'e', - '/ƒ/' => 'f', - '/ĝ|ğ|ġ|ģ|ґ/' => 'g', - '/ĥ|ħ/' => 'h', - '/ì|í|î|ï|ĩ|ī|ĭ|ǐ|į|ı|і/' => 'i', - '/ij/' => 'ij', - '/ĵ/' => 'j', - '/ķ/' => 'k', - '/ĺ|ļ|ľ|ŀ|ł/' => 'l', - '/ñ|ń|ņ|ň|ʼn/' => 'n', - '/ò|ó|ô|õ|ō|ŏ|ǒ|ő|ơ|ø|ǿ|º/' => 'o', - '/ö|œ/' => 'oe', - '/ŕ|ŗ|ř/' => 'r', - '/ś|ŝ|ş|ș|š|ſ/' => 's', - '/ß/' => 'ss', - '/ţ|ț|ť|ŧ/' => 't', - '/þ/' => 'th', - '/ù|ú|û|ũ|ū|ŭ|ů|ű|ų|ư|ǔ|ǖ|ǘ|ǚ|ǜ/' => 'u', - '/ü/' => 'ue', - '/ŵ/' => 'w', - '/ý|ÿ|ŷ/' => 'y', - '/є/' => 'ye', - '/ї/' => 'yi', - '/ź|ż|ž/' => 'z', - ); - -/** - * Method cache array. - * - * @var array - */ - protected static $_cache = array(); - -/** - * The initial state of Inflector so reset() works. - * - * @var array - */ - protected static $_initialState = array(); - -/** - * Cache inflected values, and return if already available - * - * @param string $type Inflection type - * @param string $key Original value - * @param string $value Inflected value - * @return string Inflected value, from cache - */ - protected static function _cache($type, $key, $value = false) { - $key = '_' . $key; - $type = '_' . $type; - if ($value !== false) { - static::$_cache[$type][$key] = $value; - return $value; - } - if (!isset(static::$_cache[$type][$key])) { - return false; - } - return static::$_cache[$type][$key]; - } - -/** - * Clears Inflectors inflected value caches. And resets the inflection - * rules to the initial values. - * - * @return void - */ - public static function reset() { - if (empty(static::$_initialState)) { - static::$_initialState = get_class_vars('Inflector'); - return; - } - foreach (static::$_initialState as $key => $val) { - if ($key !== '_initialState') { - static::${$key} = $val; - } - } - } - -/** - * Adds custom inflection $rules, of either 'plural', 'singular' or 'transliteration' $type. - * - * ### Usage: - * - * ``` - * Inflector::rules('plural', array('/^(inflect)or$/i' => '\1ables')); - * Inflector::rules('plural', array( - * 'rules' => array('/^(inflect)ors$/i' => '\1ables'), - * 'uninflected' => array('dontinflectme'), - * 'irregular' => array('red' => 'redlings') - * )); - * Inflector::rules('transliteration', array('/å/' => 'aa')); - * ``` - * - * @param string $type The type of inflection, either 'plural', 'singular' or 'transliteration' - * @param array $rules Array of rules to be added. - * @param bool $reset If true, will unset default inflections for all - * new rules that are being defined in $rules. - * @return void - */ - public static function rules($type, $rules, $reset = false) { - $var = '_' . $type; - - switch ($type) { - case 'transliteration': - if ($reset) { - static::$_transliteration = $rules; - } else { - static::$_transliteration = $rules + static::$_transliteration; - } - break; - - default: - foreach ($rules as $rule => $pattern) { - if (is_array($pattern)) { - if ($reset) { - static::${$var}[$rule] = $pattern; - } else { - if ($rule === 'uninflected') { - static::${$var}[$rule] = array_merge($pattern, static::${$var}[$rule]); - } else { - static::${$var}[$rule] = $pattern + static::${$var}[$rule]; - } - } - unset($rules[$rule], static::${$var}['cache' . ucfirst($rule)]); - if (isset(static::${$var}['merged'][$rule])) { - unset(static::${$var}['merged'][$rule]); - } - if ($type === 'plural') { - static::$_cache['pluralize'] = static::$_cache['tableize'] = array(); - } elseif ($type === 'singular') { - static::$_cache['singularize'] = array(); - } - } - } - static::${$var}['rules'] = $rules + static::${$var}['rules']; - } - } - -/** - * Return $word in plural form. - * - * @param string $word Word in singular - * @return string Word in plural - * @link https://book.cakephp.org/2.0/en/core-utility-libraries/inflector.html#Inflector::pluralize - */ - public static function pluralize($word) { - if (isset(static::$_cache['pluralize'][$word])) { - return static::$_cache['pluralize'][$word]; - } - - if (!isset(static::$_plural['merged']['irregular'])) { - static::$_plural['merged']['irregular'] = static::$_plural['irregular']; - } - - if (!isset(static::$_plural['merged']['uninflected'])) { - static::$_plural['merged']['uninflected'] = array_merge(static::$_plural['uninflected'], static::$_uninflected); - } - - if (!isset(static::$_plural['cacheUninflected']) || !isset(static::$_plural['cacheIrregular'])) { - static::$_plural['cacheUninflected'] = '(?:' . implode('|', static::$_plural['merged']['uninflected']) . ')'; - static::$_plural['cacheIrregular'] = '(?:' . implode('|', array_keys(static::$_plural['merged']['irregular'])) . ')'; - } - - if (preg_match('/(.*?(?:\\b|_))(' . static::$_plural['cacheIrregular'] . ')$/i', $word, $regs)) { - static::$_cache['pluralize'][$word] = $regs[1] . - substr($regs[2], 0, 1) . - substr(static::$_plural['merged']['irregular'][strtolower($regs[2])], 1); - return static::$_cache['pluralize'][$word]; - } - - if (preg_match('/^(' . static::$_plural['cacheUninflected'] . ')$/i', $word, $regs)) { - static::$_cache['pluralize'][$word] = $word; - return $word; - } - - foreach (static::$_plural['rules'] as $rule => $replacement) { - if (preg_match($rule, $word)) { - static::$_cache['pluralize'][$word] = preg_replace($rule, $replacement, $word); - return static::$_cache['pluralize'][$word]; - } - } - } - -/** - * Return $word in singular form. - * - * @param string $word Word in plural - * @return string Word in singular - * @link https://book.cakephp.org/2.0/en/core-utility-libraries/inflector.html#Inflector::singularize - */ - public static function singularize($word) { - if (isset(static::$_cache['singularize'][$word])) { - return static::$_cache['singularize'][$word]; - } - - if (!isset(static::$_singular['merged']['uninflected'])) { - static::$_singular['merged']['uninflected'] = array_merge( - static::$_singular['uninflected'], - static::$_uninflected - ); - } - - if (!isset(static::$_singular['merged']['irregular'])) { - static::$_singular['merged']['irregular'] = array_merge( - static::$_singular['irregular'], - array_flip(static::$_plural['irregular']) - ); - } - - if (!isset(static::$_singular['cacheUninflected']) || !isset(static::$_singular['cacheIrregular'])) { - static::$_singular['cacheUninflected'] = '(?:' . implode('|', static::$_singular['merged']['uninflected']) . ')'; - static::$_singular['cacheIrregular'] = '(?:' . implode('|', array_keys(static::$_singular['merged']['irregular'])) . ')'; - } - - if (preg_match('/(.*?(?:\\b|_))(' . static::$_singular['cacheIrregular'] . ')$/i', $word, $regs)) { - static::$_cache['singularize'][$word] = $regs[1] . - substr($regs[2], 0, 1) . - substr(static::$_singular['merged']['irregular'][strtolower($regs[2])], 1); - return static::$_cache['singularize'][$word]; - } - - if (preg_match('/^(' . static::$_singular['cacheUninflected'] . ')$/i', $word, $regs)) { - static::$_cache['singularize'][$word] = $word; - return $word; - } - - foreach (static::$_singular['rules'] as $rule => $replacement) { - if (preg_match($rule, $word)) { - static::$_cache['singularize'][$word] = preg_replace($rule, $replacement, $word); - return static::$_cache['singularize'][$word]; - } - } - static::$_cache['singularize'][$word] = $word; - return $word; - } - -/** - * Returns the given lower_case_and_underscored_word as a CamelCased word. - * - * @param string $lowerCaseAndUnderscoredWord Word to camelize - * @return string Camelized word. LikeThis. - * @link https://book.cakephp.org/2.0/en/core-utility-libraries/inflector.html#Inflector::camelize - */ - public static function camelize($lowerCaseAndUnderscoredWord) { - if (!($result = static::_cache(__FUNCTION__, $lowerCaseAndUnderscoredWord))) { - $result = str_replace(' ', '', Inflector::humanize($lowerCaseAndUnderscoredWord)); - static::_cache(__FUNCTION__, $lowerCaseAndUnderscoredWord, $result); - } - return $result; - } - -/** - * Returns the given camelCasedWord as an underscored_word. - * - * @param string $camelCasedWord Camel-cased word to be "underscorized" - * @return string Underscore-syntaxed version of the $camelCasedWord - * @link https://book.cakephp.org/2.0/en/core-utility-libraries/inflector.html#Inflector::underscore - */ - public static function underscore($camelCasedWord) { - if (!($result = static::_cache(__FUNCTION__, $camelCasedWord))) { - $underscoredWord = preg_replace('/(?<=\\w)([A-Z])/', '_\\1', $camelCasedWord); - $result = mb_strtolower($underscoredWord); - static::_cache(__FUNCTION__, $camelCasedWord, $result); - } - return $result; - } - -/** - * Returns the given underscored_word_group as a Human Readable Word Group. - * (Underscores are replaced by spaces and capitalized following words.) - * - * @param string $lowerCaseAndUnderscoredWord String to be made more readable - * @return string Human-readable string - * @link https://book.cakephp.org/2.0/en/core-utility-libraries/inflector.html#Inflector::humanize - */ - public static function humanize($lowerCaseAndUnderscoredWord) { - if (!($result = static::_cache(__FUNCTION__, $lowerCaseAndUnderscoredWord))) { - $result = explode(' ', str_replace('_', ' ', $lowerCaseAndUnderscoredWord)); - foreach ($result as &$word) { - $word = mb_strtoupper(mb_substr($word, 0, 1)) . mb_substr($word, 1); - } - $result = implode(' ', $result); - static::_cache(__FUNCTION__, $lowerCaseAndUnderscoredWord, $result); - } - return $result; - } - -/** - * Returns corresponding table name for given model $className. ("people" for the model class "Person"). - * - * @param string $className Name of class to get database table name for - * @return string Name of the database table for given class - * @link https://book.cakephp.org/2.0/en/core-utility-libraries/inflector.html#Inflector::tableize - */ - public static function tableize($className) { - if (!($result = static::_cache(__FUNCTION__, $className))) { - $result = Inflector::pluralize(Inflector::underscore($className)); - static::_cache(__FUNCTION__, $className, $result); - } - return $result; - } - -/** - * Returns Cake model class name ("Person" for the database table "people".) for given database table. - * - * @param string $tableName Name of database table to get class name for - * @return string Class name - * @link https://book.cakephp.org/2.0/en/core-utility-libraries/inflector.html#Inflector::classify - */ - public static function classify($tableName) { - if (!($result = static::_cache(__FUNCTION__, $tableName))) { - $result = Inflector::camelize(Inflector::singularize($tableName)); - static::_cache(__FUNCTION__, $tableName, $result); - } - return $result; - } - -/** - * Returns camelBacked version of an underscored string. - * - * @param string $string String to convert. - * @return string in variable form - * @link https://book.cakephp.org/2.0/en/core-utility-libraries/inflector.html#Inflector::variable - */ - public static function variable($string) { - if (!($result = static::_cache(__FUNCTION__, $string))) { - $camelized = Inflector::camelize(Inflector::underscore($string)); - $replace = strtolower(substr($camelized, 0, 1)); - $result = preg_replace('/\\w/', $replace, $camelized, 1); - static::_cache(__FUNCTION__, $string, $result); - } - return $result; - } - -/** - * Returns a string with all spaces converted to underscores (by default), accented - * characters converted to non-accented characters, and non word characters removed. - * - * @param string $string the string you want to slug - * @param string $replacement will replace keys in map - * @return string - * @link https://book.cakephp.org/2.0/en/core-utility-libraries/inflector.html#Inflector::slug - */ - public static function slug($string, $replacement = '_') { - $quotedReplacement = preg_quote($replacement, '/'); - - $merge = array( - '/[^\s\p{Zs}\p{Ll}\p{Lm}\p{Lo}\p{Lt}\p{Lu}\p{Nd}]/mu' => ' ', - '/[\s\p{Zs}]+/mu' => $replacement, - sprintf('/^[%s]+|[%s]+$/', $quotedReplacement, $quotedReplacement) => '', - ); - - $map = static::$_transliteration + $merge; - return preg_replace(array_keys($map), array_values($map), $string); - } +class Inflector +{ + + /** + * Plural inflector rules + * + * @var array + */ + protected static $_plural = [ + 'rules' => [ + '/(s)tatus$/i' => '\1tatuses', + '/(quiz)$/i' => '\1zes', + '/^(ox)$/i' => '\1\2en', + '/([m|l])ouse$/i' => '\1ice', + '/(matr|vert|ind)(ix|ex)$/i' => '\1ices', + '/(x|ch|ss|sh)$/i' => '\1es', + '/([^aeiouy]|qu)y$/i' => '\1ies', + '/(hive)$/i' => '\1s', + '/(?:([^f])fe|([lre])f)$/i' => '\1\2ves', + '/sis$/i' => 'ses', + '/([ti])um$/i' => '\1a', + '/(p)erson$/i' => '\1eople', + '/(? '\1en', + '/(c)hild$/i' => '\1hildren', + '/(buffal|tomat)o$/i' => '\1\2oes', + '/(alumn|bacill|cact|foc|fung|nucle|radi|stimul|syllab|termin)us$/i' => '\1i', + '/us$/i' => 'uses', + '/(alias)$/i' => '\1es', + '/(ax|cris|test)is$/i' => '\1es', + '/s$/' => 's', + '/^$/' => '', + '/$/' => 's', + ], + 'uninflected' => [ + '.*[nrlm]ese', + '.*data', + '.*deer', + '.*fish', + '.*measles', + '.*ois', + '.*pox', + '.*sheep', + 'people', + 'feedback', + 'stadia' + ], + 'irregular' => [ + 'atlas' => 'atlases', + 'beef' => 'beefs', + 'brief' => 'briefs', + 'brother' => 'brothers', + 'cafe' => 'cafes', + 'child' => 'children', + 'cookie' => 'cookies', + 'corpus' => 'corpuses', + 'cow' => 'cows', + 'criterion' => 'criteria', + 'ganglion' => 'ganglions', + 'genie' => 'genies', + 'genus' => 'genera', + 'graffito' => 'graffiti', + 'hoof' => 'hoofs', + 'loaf' => 'loaves', + 'man' => 'men', + 'money' => 'monies', + 'mongoose' => 'mongooses', + 'move' => 'moves', + 'mythos' => 'mythoi', + 'niche' => 'niches', + 'numen' => 'numina', + 'occiput' => 'occiputs', + 'octopus' => 'octopuses', + 'opus' => 'opuses', + 'ox' => 'oxen', + 'penis' => 'penises', + 'person' => 'people', + 'sex' => 'sexes', + 'soliloquy' => 'soliloquies', + 'testis' => 'testes', + 'trilby' => 'trilbys', + 'turf' => 'turfs', + 'potato' => 'potatoes', + 'hero' => 'heroes', + 'tooth' => 'teeth', + 'goose' => 'geese', + 'foot' => 'feet', + 'sieve' => 'sieves' + ] + ]; + + /** + * Singular inflector rules + * + * @var array + */ + protected static $_singular = [ + 'rules' => [ + '/(s)tatuses$/i' => '\1\2tatus', + '/^(.*)(menu)s$/i' => '\1\2', + '/(quiz)zes$/i' => '\\1', + '/(matr)ices$/i' => '\1ix', + '/(vert|ind)ices$/i' => '\1ex', + '/^(ox)en/i' => '\1', + '/(alias)(es)*$/i' => '\1', + '/(alumn|bacill|cact|foc|fung|nucle|radi|stimul|syllab|termin|viri?)i$/i' => '\1us', + '/([ftw]ax)es/i' => '\1', + '/(cris|ax|test)es$/i' => '\1is', + '/(shoe)s$/i' => '\1', + '/(o)es$/i' => '\1', + '/ouses$/' => 'ouse', + '/([^a])uses$/' => '\1us', + '/([m|l])ice$/i' => '\1ouse', + '/(x|ch|ss|sh)es$/i' => '\1', + '/(m)ovies$/i' => '\1\2ovie', + '/(s)eries$/i' => '\1\2eries', + '/([^aeiouy]|qu)ies$/i' => '\1y', + '/(tive)s$/i' => '\1', + '/(hive)s$/i' => '\1', + '/(drive)s$/i' => '\1', + '/([le])ves$/i' => '\1f', + '/([^rfoa])ves$/i' => '\1fe', + '/(^analy)ses$/i' => '\1sis', + '/(analy|diagno|^ba|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$/i' => '\1\2sis', + '/([ti])a$/i' => '\1um', + '/(p)eople$/i' => '\1\2erson', + '/(m)en$/i' => '\1an', + '/(c)hildren$/i' => '\1\2hild', + '/(n)ews$/i' => '\1\2ews', + '/eaus$/' => 'eau', + '/^(.*us)$/' => '\\1', + '/s$/i' => '' + ], + 'uninflected' => [ + '.*data', + '.*[nrlm]ese', '.*deer', '.*fish', '.*measles', '.*ois', '.*pox', '.*sheep', '.*ss', 'feedback' + ], + 'irregular' => [ + 'foes' => 'foe', + ] + ]; + + /** + * Words that should not be inflected + * + * @var array + */ + protected static $_uninflected = [ + 'Amoyese', 'bison', 'Borghese', 'bream', 'breeches', 'britches', 'buffalo', 'cantus', + 'carp', 'chassis', 'clippers', 'cod', 'coitus', 'Congoese', 'contretemps', 'corps', + 'debris', 'diabetes', 'djinn', 'eland', 'elk', 'equipment', 'Faroese', 'flounder', + 'Foochowese', 'gallows', 'Genevese', 'Genoese', 'Gilbertese', 'graffiti', + 'headquarters', 'herpes', 'hijinks', 'Hottentotese', 'information', 'innings', + 'jackanapes', 'Kiplingese', 'Kongoese', 'Lucchese', 'mackerel', 'Maltese', '.*?media', + 'mews', 'moose', 'mumps', 'Nankingese', 'news', 'nexus', 'Niasese', + 'Pekingese', 'Piedmontese', 'pincers', 'Pistoiese', 'pliers', 'Portuguese', + 'proceedings', 'rabies', 'research', 'rice', 'rhinoceros', 'salmon', 'Sarawakese', 'scissors', + 'sea[- ]bass', 'series', 'Shavese', 'shears', 'siemens', 'species', 'swine', 'testes', + 'trousers', 'trout', 'tuna', 'Vermontese', 'Wenchowese', 'whiting', 'wildebeest', + 'Yengeese' + ]; + + /** + * Default map of accented and special characters to ASCII characters + * + * @var array + */ + protected static $_transliteration = [ + '/À|Á|Â|Ã|Å|Ǻ|Ā|Ă|Ą|Ǎ/' => 'A', + '/Æ|Ǽ/' => 'AE', + '/Ä/' => 'Ae', + '/Ç|Ć|Ĉ|Ċ|Č/' => 'C', + '/Ð|Ď|Đ/' => 'D', + '/È|É|Ê|Ë|Ē|Ĕ|Ė|Ę|Ě/' => 'E', + '/Ĝ|Ğ|Ġ|Ģ|Ґ/' => 'G', + '/Ĥ|Ħ/' => 'H', + '/Ì|Í|Î|Ï|Ĩ|Ī|Ĭ|Ǐ|Į|İ|І/' => 'I', + '/IJ/' => 'IJ', + '/Ĵ/' => 'J', + '/Ķ/' => 'K', + '/Ĺ|Ļ|Ľ|Ŀ|Ł/' => 'L', + '/Ñ|Ń|Ņ|Ň/' => 'N', + '/Ò|Ó|Ô|Õ|Ō|Ŏ|Ǒ|Ő|Ơ|Ø|Ǿ/' => 'O', + '/Œ/' => 'OE', + '/Ö/' => 'Oe', + '/Ŕ|Ŗ|Ř/' => 'R', + '/Ś|Ŝ|Ş|Ș|Š/' => 'S', + '/ẞ/' => 'SS', + '/Ţ|Ț|Ť|Ŧ/' => 'T', + '/Þ/' => 'TH', + '/Ù|Ú|Û|Ũ|Ū|Ŭ|Ů|Ű|Ų|Ư|Ǔ|Ǖ|Ǘ|Ǚ|Ǜ/' => 'U', + '/Ü/' => 'Ue', + '/Ŵ/' => 'W', + '/Ý|Ÿ|Ŷ/' => 'Y', + '/Є/' => 'Ye', + '/Ї/' => 'Yi', + '/Ź|Ż|Ž/' => 'Z', + '/à|á|â|ã|å|ǻ|ā|ă|ą|ǎ|ª/' => 'a', + '/ä|æ|ǽ/' => 'ae', + '/ç|ć|ĉ|ċ|č/' => 'c', + '/ð|ď|đ/' => 'd', + '/è|é|ê|ë|ē|ĕ|ė|ę|ě/' => 'e', + '/ƒ/' => 'f', + '/ĝ|ğ|ġ|ģ|ґ/' => 'g', + '/ĥ|ħ/' => 'h', + '/ì|í|î|ï|ĩ|ī|ĭ|ǐ|į|ı|і/' => 'i', + '/ij/' => 'ij', + '/ĵ/' => 'j', + '/ķ/' => 'k', + '/ĺ|ļ|ľ|ŀ|ł/' => 'l', + '/ñ|ń|ņ|ň|ʼn/' => 'n', + '/ò|ó|ô|õ|ō|ŏ|ǒ|ő|ơ|ø|ǿ|º/' => 'o', + '/ö|œ/' => 'oe', + '/ŕ|ŗ|ř/' => 'r', + '/ś|ŝ|ş|ș|š|ſ/' => 's', + '/ß/' => 'ss', + '/ţ|ț|ť|ŧ/' => 't', + '/þ/' => 'th', + '/ù|ú|û|ũ|ū|ŭ|ů|ű|ų|ư|ǔ|ǖ|ǘ|ǚ|ǜ/' => 'u', + '/ü/' => 'ue', + '/ŵ/' => 'w', + '/ý|ÿ|ŷ/' => 'y', + '/є/' => 'ye', + '/ї/' => 'yi', + '/ź|ż|ž/' => 'z', + ]; + + /** + * Method cache array. + * + * @var array + */ + protected static $_cache = []; + + /** + * The initial state of Inflector so reset() works. + * + * @var array + */ + protected static $_initialState = []; + + /** + * Clears Inflectors inflected value caches. And resets the inflection + * rules to the initial values. + * + * @return void + */ + public static function reset() + { + if (empty(static::$_initialState)) { + static::$_initialState = get_class_vars('Inflector'); + return; + } + foreach (static::$_initialState as $key => $val) { + if ($key !== '_initialState') { + static::${$key} = $val; + } + } + } + + /** + * Adds custom inflection $rules, of either 'plural', 'singular' or 'transliteration' $type. + * + * ### Usage: + * + * ``` + * Inflector::rules('plural', array('/^(inflect)or$/i' => '\1ables')); + * Inflector::rules('plural', array( + * 'rules' => array('/^(inflect)ors$/i' => '\1ables'), + * 'uninflected' => array('dontinflectme'), + * 'irregular' => array('red' => 'redlings') + * )); + * Inflector::rules('transliteration', array('/å/' => 'aa')); + * ``` + * + * @param string $type The type of inflection, either 'plural', 'singular' or 'transliteration' + * @param array $rules Array of rules to be added. + * @param bool $reset If true, will unset default inflections for all + * new rules that are being defined in $rules. + * @return void + */ + public static function rules($type, $rules, $reset = false) + { + $var = '_' . $type; + + switch ($type) { + case 'transliteration': + if ($reset) { + static::$_transliteration = $rules; + } else { + static::$_transliteration = $rules + static::$_transliteration; + } + break; + + default: + foreach ($rules as $rule => $pattern) { + if (is_array($pattern)) { + if ($reset) { + static::${$var}[$rule] = $pattern; + } else { + if ($rule === 'uninflected') { + static::${$var}[$rule] = array_merge($pattern, static::${$var}[$rule]); + } else { + static::${$var}[$rule] = $pattern + static::${$var}[$rule]; + } + } + unset($rules[$rule], static::${$var}['cache' . ucfirst($rule)]); + if (isset(static::${$var}['merged'][$rule])) { + unset(static::${$var}['merged'][$rule]); + } + if ($type === 'plural') { + static::$_cache['pluralize'] = static::$_cache['tableize'] = []; + } else if ($type === 'singular') { + static::$_cache['singularize'] = []; + } + } + } + static::${$var}['rules'] = $rules + static::${$var}['rules']; + } + } + + /** + * Returns corresponding table name for given model $className. ("people" for the model class "Person"). + * + * @param string $className Name of class to get database table name for + * @return string Name of the database table for given class + * @link https://book.cakephp.org/2.0/en/core-utility-libraries/inflector.html#Inflector::tableize + */ + public static function tableize($className) + { + if (!($result = static::_cache(__FUNCTION__, $className))) { + $result = Inflector::pluralize(Inflector::underscore($className)); + static::_cache(__FUNCTION__, $className, $result); + } + return $result; + } + + /** + * Cache inflected values, and return if already available + * + * @param string $type Inflection type + * @param string $key Original value + * @param string $value Inflected value + * @return string Inflected value, from cache + */ + protected static function _cache($type, $key, $value = false) + { + $key = '_' . $key; + $type = '_' . $type; + if ($value !== false) { + static::$_cache[$type][$key] = $value; + return $value; + } + if (!isset(static::$_cache[$type][$key])) { + return false; + } + return static::$_cache[$type][$key]; + } + + /** + * Return $word in plural form. + * + * @param string $word Word in singular + * @return string Word in plural + * @link https://book.cakephp.org/2.0/en/core-utility-libraries/inflector.html#Inflector::pluralize + */ + public static function pluralize($word) + { + if (isset(static::$_cache['pluralize'][$word])) { + return static::$_cache['pluralize'][$word]; + } + + if (!isset(static::$_plural['merged']['irregular'])) { + static::$_plural['merged']['irregular'] = static::$_plural['irregular']; + } + + if (!isset(static::$_plural['merged']['uninflected'])) { + static::$_plural['merged']['uninflected'] = array_merge(static::$_plural['uninflected'], static::$_uninflected); + } + + if (!isset(static::$_plural['cacheUninflected']) || !isset(static::$_plural['cacheIrregular'])) { + static::$_plural['cacheUninflected'] = '(?:' . implode('|', static::$_plural['merged']['uninflected']) . ')'; + static::$_plural['cacheIrregular'] = '(?:' . implode('|', array_keys(static::$_plural['merged']['irregular'])) . ')'; + } + + if (preg_match('/(.*?(?:\\b|_))(' . static::$_plural['cacheIrregular'] . ')$/i', $word, $regs)) { + static::$_cache['pluralize'][$word] = $regs[1] . + substr($regs[2], 0, 1) . + substr(static::$_plural['merged']['irregular'][strtolower($regs[2])], 1); + return static::$_cache['pluralize'][$word]; + } + + if (preg_match('/^(' . static::$_plural['cacheUninflected'] . ')$/i', $word, $regs)) { + static::$_cache['pluralize'][$word] = $word; + return $word; + } + + foreach (static::$_plural['rules'] as $rule => $replacement) { + if (preg_match($rule, $word)) { + static::$_cache['pluralize'][$word] = preg_replace($rule, $replacement, $word); + return static::$_cache['pluralize'][$word]; + } + } + } + + /** + * Returns the given camelCasedWord as an underscored_word. + * + * @param string $camelCasedWord Camel-cased word to be "underscorized" + * @return string Underscore-syntaxed version of the $camelCasedWord + * @link https://book.cakephp.org/2.0/en/core-utility-libraries/inflector.html#Inflector::underscore + */ + public static function underscore($camelCasedWord) + { + if (!($result = static::_cache(__FUNCTION__, $camelCasedWord))) { + $underscoredWord = preg_replace('/(?<=\\w)([A-Z])/', '_\\1', $camelCasedWord); + $result = mb_strtolower($underscoredWord); + static::_cache(__FUNCTION__, $camelCasedWord, $result); + } + return $result; + } + + /** + * Returns Cake model class name ("Person" for the database table "people".) for given database table. + * + * @param string $tableName Name of database table to get class name for + * @return string Class name + * @link https://book.cakephp.org/2.0/en/core-utility-libraries/inflector.html#Inflector::classify + */ + public static function classify($tableName) + { + if (!($result = static::_cache(__FUNCTION__, $tableName))) { + $result = Inflector::camelize(Inflector::singularize($tableName)); + static::_cache(__FUNCTION__, $tableName, $result); + } + return $result; + } + + /** + * Returns the given lower_case_and_underscored_word as a CamelCased word. + * + * @param string $lowerCaseAndUnderscoredWord Word to camelize + * @return string Camelized word. LikeThis. + * @link https://book.cakephp.org/2.0/en/core-utility-libraries/inflector.html#Inflector::camelize + */ + public static function camelize($lowerCaseAndUnderscoredWord) + { + if (!($result = static::_cache(__FUNCTION__, $lowerCaseAndUnderscoredWord))) { + $result = str_replace(' ', '', Inflector::humanize($lowerCaseAndUnderscoredWord)); + static::_cache(__FUNCTION__, $lowerCaseAndUnderscoredWord, $result); + } + return $result; + } + + /** + * Returns the given underscored_word_group as a Human Readable Word Group. + * (Underscores are replaced by spaces and capitalized following words.) + * + * @param string $lowerCaseAndUnderscoredWord String to be made more readable + * @return string Human-readable string + * @link https://book.cakephp.org/2.0/en/core-utility-libraries/inflector.html#Inflector::humanize + */ + public static function humanize($lowerCaseAndUnderscoredWord) + { + if (!($result = static::_cache(__FUNCTION__, $lowerCaseAndUnderscoredWord))) { + $result = explode(' ', str_replace('_', ' ', $lowerCaseAndUnderscoredWord)); + foreach ($result as &$word) { + $word = mb_strtoupper(mb_substr($word, 0, 1)) . mb_substr($word, 1); + } + $result = implode(' ', $result); + static::_cache(__FUNCTION__, $lowerCaseAndUnderscoredWord, $result); + } + return $result; + } + + /** + * Return $word in singular form. + * + * @param string $word Word in plural + * @return string Word in singular + * @link https://book.cakephp.org/2.0/en/core-utility-libraries/inflector.html#Inflector::singularize + */ + public static function singularize($word) + { + if (isset(static::$_cache['singularize'][$word])) { + return static::$_cache['singularize'][$word]; + } + + if (!isset(static::$_singular['merged']['uninflected'])) { + static::$_singular['merged']['uninflected'] = array_merge( + static::$_singular['uninflected'], + static::$_uninflected + ); + } + + if (!isset(static::$_singular['merged']['irregular'])) { + static::$_singular['merged']['irregular'] = array_merge( + static::$_singular['irregular'], + array_flip(static::$_plural['irregular']) + ); + } + + if (!isset(static::$_singular['cacheUninflected']) || !isset(static::$_singular['cacheIrregular'])) { + static::$_singular['cacheUninflected'] = '(?:' . implode('|', static::$_singular['merged']['uninflected']) . ')'; + static::$_singular['cacheIrregular'] = '(?:' . implode('|', array_keys(static::$_singular['merged']['irregular'])) . ')'; + } + + if (preg_match('/(.*?(?:\\b|_))(' . static::$_singular['cacheIrregular'] . ')$/i', $word, $regs)) { + static::$_cache['singularize'][$word] = $regs[1] . + substr($regs[2], 0, 1) . + substr(static::$_singular['merged']['irregular'][strtolower($regs[2])], 1); + return static::$_cache['singularize'][$word]; + } + + if (preg_match('/^(' . static::$_singular['cacheUninflected'] . ')$/i', $word, $regs)) { + static::$_cache['singularize'][$word] = $word; + return $word; + } + + foreach (static::$_singular['rules'] as $rule => $replacement) { + if (preg_match($rule, $word)) { + static::$_cache['singularize'][$word] = preg_replace($rule, $replacement, $word); + return static::$_cache['singularize'][$word]; + } + } + static::$_cache['singularize'][$word] = $word; + return $word; + } + + /** + * Returns camelBacked version of an underscored string. + * + * @param string $string String to convert. + * @return string in variable form + * @link https://book.cakephp.org/2.0/en/core-utility-libraries/inflector.html#Inflector::variable + */ + public static function variable($string) + { + if (!($result = static::_cache(__FUNCTION__, $string))) { + $camelized = Inflector::camelize(Inflector::underscore($string)); + $replace = strtolower(substr($camelized, 0, 1)); + $result = preg_replace('/\\w/', $replace, $camelized, 1); + static::_cache(__FUNCTION__, $string, $result); + } + return $result; + } + + /** + * Returns a string with all spaces converted to underscores (by default), accented + * characters converted to non-accented characters, and non word characters removed. + * + * @param string $string the string you want to slug + * @param string $replacement will replace keys in map + * @return string + * @link https://book.cakephp.org/2.0/en/core-utility-libraries/inflector.html#Inflector::slug + */ + public static function slug($string, $replacement = '_') + { + $quotedReplacement = preg_quote($replacement, '/'); + + $merge = [ + '/[^\s\p{Zs}\p{Ll}\p{Lm}\p{Lo}\p{Lt}\p{Lu}\p{Nd}]/mu' => ' ', + '/[\s\p{Zs}]+/mu' => $replacement, + sprintf('/^[%s]+|[%s]+$/', $quotedReplacement, $quotedReplacement) => '', + ]; + + $map = static::$_transliteration + $merge; + return preg_replace(array_keys($map), array_values($map), $string); + } } diff --git a/lib/Cake/Utility/ObjectCollection.php b/lib/Cake/Utility/ObjectCollection.php index 98e137da..e616ecf6 100755 --- a/lib/Cake/Utility/ObjectCollection.php +++ b/lib/Cake/Utility/ObjectCollection.php @@ -25,28 +25,49 @@ * @package Cake.Utility * @since CakePHP(tm) v 2.0 */ -abstract class ObjectCollection { +abstract class ObjectCollection +{ + /** + * Default object priority. A non zero integer. + * + * @var int + */ + public $defaultPriority = 10; /** * List of the currently-enabled objects * * @var array */ - protected $_enabled = array(); - + protected $_enabled = []; /** * A hash of loaded objects, indexed by name * * @var array */ - protected $_loaded = array(); + protected $_loaded = []; /** - * Default object priority. A non zero integer. + * Normalizes an object array, creates an array that makes lazy loading + * easier * - * @var int + * @param array $objects Array of child objects to normalize. + * @return array Array of normalized objects. */ - public $defaultPriority = 10; + public static function normalizeObjectArray($objects) + { + $normal = []; + foreach ($objects as $i => $objectName) { + $options = []; + if (!is_int($i)) { + $options = (array)$objectName; + $objectName = $i; + } + list(, $name) = pluginSplit($objectName); + $normal[$name] = ['class' => $objectName, 'settings' => $options]; + } + return $normal; + } /** * Loads a new object onto the collection. Can throw a variety of exceptions @@ -58,7 +79,7 @@ abstract class ObjectCollection { * @param array $options Array of configuration options for the object to be constructed. * @return CakeObject the constructed object */ - abstract public function load($name, $options = array()); + abstract public function load($name, $options = []); /** * Trigger a callback method on every object in the collection. @@ -91,7 +112,8 @@ abstract public function load($name, $options = array()); * @return mixed Either the last result or all results if collectReturn is on. * @throws CakeException when modParams is used with an index that does not exist. */ - public function trigger($callback, $params = array(), $options = array()) { + public function trigger($callback, $params = [], $options = []) + { if (empty($this->_enabled)) { return true; } @@ -105,7 +127,7 @@ public function trigger($callback, $params = array(), $options = array()) { $subject = $event->subject(); } - foreach (array('break', 'breakOn', 'collectReturn', 'modParams') as $opt) { + foreach (['break', 'breakOn', 'collectReturn', 'modParams'] as $opt) { if (isset($event->{$opt})) { $options[$opt] = $event->{$opt}; } @@ -113,20 +135,20 @@ public function trigger($callback, $params = array(), $options = array()) { $parts = explode('.', $event->name()); $callback = array_pop($parts); } - $options += array( + $options += [ 'break' => false, 'breakOn' => false, 'collectReturn' => false, 'modParams' => false - ); - $collected = array(); + ]; + $collected = []; $list = array_keys($this->_enabled); if ($options['modParams'] !== false && !isset($params[$options['modParams']])) { throw new CakeException(__d('cake_dev', 'Cannot use modParams with indexes that do not exist.')); } $result = null; foreach ($list as $name) { - $result = call_user_func_array(array($this->_loaded[$name], $callback), array_values(array_filter(compact('subject')) + $params)); + $result = call_user_func_array([$this->_loaded[$name], $callback], array_values(array_filter(compact('subject')) + $params)); if ($options['collectReturn'] === true) { $collected[] = $result; } @@ -134,7 +156,7 @@ public function trigger($callback, $params = array(), $options = array()) { (is_array($options['breakOn']) && in_array($result, $options['breakOn'], true))) ) { return $result; - } elseif ($options['modParams'] !== false && !in_array($result, array(true, false, null), true)) { + } else if ($options['modParams'] !== false && !in_array($result, [true, false, null], true)) { $params[$options['modParams']] = $result; } } @@ -150,7 +172,8 @@ public function trigger($callback, $params = array(), $options = array()) { * @param string $name Name of property to read * @return mixed */ - public function __get($name) { + public function __get($name) + { if (isset($this->_loaded[$name])) { return $this->_loaded[$name]; } @@ -163,7 +186,8 @@ public function __get($name) { * @param string $name Name of object being checked. * @return bool */ - public function __isset($name) { + public function __isset($name) + { return isset($this->_loaded[$name]); } @@ -174,7 +198,8 @@ public function __isset($name) { * @param bool $prioritize Prioritize enabled list after enabling object(s) * @return void */ - public function enable($name, $prioritize = true) { + public function enable($name, $prioritize = true) + { $enabled = false; foreach ((array)$name as $object) { list(, $object) = pluginSplit($object); @@ -183,7 +208,7 @@ public function enable($name, $prioritize = true) { if (isset($this->_loaded[$object]->settings['priority'])) { $priority = $this->_loaded[$object]->settings['priority']; } - $this->_enabled[$object] = array($priority); + $this->_enabled[$object] = [$priority]; $enabled = true; } } @@ -197,7 +222,8 @@ public function enable($name, $prioritize = true) { * * @return array Prioritized list of object */ - public function prioritize() { + public function prioritize() + { $i = 1; foreach ($this->_enabled as $name => $priority) { $priority[1] = $i++; @@ -211,14 +237,15 @@ public function prioritize() { * Set priority for an object or array of objects * * @param string|array $name CamelCased name of the object(s) to enable (string or array) - * If string the second param $priority is used else it should be an associative array - * with keys as object names and values as priorities to set. + * If string the second param $priority is used else it should be an associative array + * with keys as object names and values as priorities to set. * @param int|null $priority Integer priority to set or null for default * @return void */ - public function setPriority($name, $priority = null) { + public function setPriority($name, $priority = null) + { if (is_string($name)) { - $name = array($name => $priority); + $name = [$name => $priority]; } foreach ($name as $object => $objectPriority) { list(, $object) = pluginSplit($object); @@ -228,7 +255,7 @@ public function setPriority($name, $priority = null) { } $this->_loaded[$object]->settings['priority'] = $objectPriority; if (isset($this->_enabled[$object])) { - $this->_enabled[$object] = array($objectPriority); + $this->_enabled[$object] = [$objectPriority]; } } } @@ -242,7 +269,8 @@ public function setPriority($name, $priority = null) { * @param string|array $name CamelCased name of the objects(s) to disable (string or array) * @return void */ - public function disable($name) { + public function disable($name) + { foreach ((array)$name as $object) { list(, $object) = pluginSplit($object); unset($this->_enabled[$object]); @@ -257,7 +285,8 @@ public function disable($name) { * @return mixed If $name is specified, returns the boolean status of the corresponding object. * Otherwise, returns an array of all enabled objects. */ - public function enabled($name = null) { + public function enabled($name = null) + { if (!empty($name)) { list(, $name) = pluginSplit($name); return isset($this->_enabled[$name]); @@ -274,7 +303,8 @@ public function enabled($name = null) { * Otherwise, returns an array of all attached objects. * @deprecated 3.0.0 Will be removed in 3.0. Use loaded instead. */ - public function attached($name = null) { + public function attached($name = null) + { return $this->loaded($name); } @@ -286,7 +316,8 @@ public function attached($name = null) { * @return mixed If $name is specified, returns the boolean status of the corresponding object. * Otherwise, returns an array of all loaded objects. */ - public function loaded($name = null) { + public function loaded($name = null) + { if (!empty($name)) { list(, $name) = pluginSplit($name); return isset($this->_loaded[$name]); @@ -300,7 +331,8 @@ public function loaded($name = null) { * @param string $name Name of the object to delete. * @return void */ - public function unload($name) { + public function unload($name) + { list(, $name) = pluginSplit($name); unset($this->_loaded[$name], $this->_enabled[$name]); } @@ -312,7 +344,8 @@ public function unload($name) { * @param CakeObject $object The object to use * @return array Loaded objects */ - public function set($name = null, $object = null) { + public function set($name = null, $object = null) + { if (!empty($name) && !empty($object)) { list(, $name) = pluginSplit($name); $this->_loaded[$name] = $object; @@ -320,25 +353,4 @@ public function set($name = null, $object = null) { return $this->_loaded; } - /** - * Normalizes an object array, creates an array that makes lazy loading - * easier - * - * @param array $objects Array of child objects to normalize. - * @return array Array of normalized objects. - */ - public static function normalizeObjectArray($objects) { - $normal = array(); - foreach ($objects as $i => $objectName) { - $options = array(); - if (!is_int($i)) { - $options = (array)$objectName; - $objectName = $i; - } - list(, $name) = pluginSplit($objectName); - $normal[$name] = array('class' => $objectName, 'settings' => $options); - } - return $normal; - } - } \ No newline at end of file diff --git a/lib/Cake/Utility/Sanitize.php b/lib/Cake/Utility/Sanitize.php index 0f8b5e07..2f2ef64a 100755 --- a/lib/Cake/Utility/Sanitize.php +++ b/lib/Cake/Utility/Sanitize.php @@ -29,241 +29,251 @@ * @package Cake.Utility * @deprecated 3.0.0 Deprecated since version 2.4 */ -class Sanitize { +class Sanitize +{ -/** - * Removes any non-alphanumeric characters. - * - * @param string|array $string String to sanitize - * @param array $allowed An array of additional characters that are not to be removed. - * @return string|array Sanitized string - */ - public static function paranoid($string, $allowed = array()) { - $allow = null; - if (!empty($allowed)) { - foreach ($allowed as $value) { - $allow .= "\\$value"; - } - } + /** + * Removes any non-alphanumeric characters. + * + * @param string|array $string String to sanitize + * @param array $allowed An array of additional characters that are not to be removed. + * @return string|array Sanitized string + */ + public static function paranoid($string, $allowed = []) + { + $allow = null; + if (!empty($allowed)) { + foreach ($allowed as $value) { + $allow .= "\\$value"; + } + } - if (!is_array($string)) { - return preg_replace("/[^{$allow}a-zA-Z0-9]/", '', $string); - } + if (!is_array($string)) { + return preg_replace("/[^{$allow}a-zA-Z0-9]/", '', $string); + } - $cleaned = array(); - foreach ($string as $key => $clean) { - $cleaned[$key] = preg_replace("/[^{$allow}a-zA-Z0-9]/", '', $clean); - } + $cleaned = []; + foreach ($string as $key => $clean) { + $cleaned[$key] = preg_replace("/[^{$allow}a-zA-Z0-9]/", '', $clean); + } - return $cleaned; - } + return $cleaned; + } -/** - * Makes a string SQL-safe. - * - * @param string $string String to sanitize - * @param string $connection Database connection being used - * @return string SQL safe string - */ - public static function escape($string, $connection = 'default') { - if (is_numeric($string) || $string === null || is_bool($string)) { - return $string; - } - $db = ConnectionManager::getDataSource($connection); - $string = $db->value($string, 'string'); - $start = 1; - if ($string{0} === 'N') { - $start = 2; - } + /** + * Strips extra whitespace, images, scripts and stylesheets from output + * + * @param string $str String to sanitize + * @return string sanitized string + */ + public static function stripAll($str) + { + return Sanitize::stripScripts( + Sanitize::stripImages( + Sanitize::stripWhitespace($str) + ) + ); + } - return substr(substr($string, $start), 0, -1); - } + /** + * Strips scripts and stylesheets from output + * + * @param string $str String to sanitize + * @return string String with , , diff --git a/lib/Cake/View/Elements/sql_dump.ctp b/lib/Cake/View/Elements/sql_dump.ctp index 0c1e748c..8b5df456 100755 --- a/lib/Cake/View/Elements/sql_dump.ctp +++ b/lib/Cake/View/Elements/sql_dump.ctp @@ -17,66 +17,73 @@ */ if (!class_exists('ConnectionManager') || Configure::read('debug') < 2) { - return false; + return false; } $noLogs = !isset($sqlLogs); if ($noLogs): - $sources = ConnectionManager::sourceList(); + $sources = ConnectionManager::sourceList(); - $sqlLogs = array(); - foreach ($sources as $source): - $db = ConnectionManager::getDataSource($source); - if (!method_exists($db, 'getLog')): - continue; - endif; - $sqlLogs[$source] = $db->getLog(); - endforeach; + $sqlLogs = []; + foreach ($sources as $source): + $db = ConnectionManager::getDataSource($source); + if (!method_exists($db, 'getLog')): + continue; + endif; + $sqlLogs[$source] = $db->getLog(); + endforeach; endif; if ($noLogs || isset($_forced_from_dbo_)): - foreach ($sqlLogs as $source => $logInfo): - $text = $logInfo['count'] > 1 ? 'queries' : 'query'; - printf( - '', - preg_replace('/[^A-Za-z0-9_]/', '_', uniqid(time(), true)) - ); - printf('', $source, $logInfo['count'], $text, $logInfo['time']); - ?> - - - - - $i) : - $i += array('error' => ''); - if (!empty($i['params']) && is_array($i['params'])) { - $bindParam = $bindType = null; - if (preg_match('/.+ :.+/', $i['query'])) { - $bindType = true; - } - foreach ($i['params'] as $bindKey => $bindVal) { - if ($bindType === true) { - $bindParam .= h($bindKey) . " => " . h($bindVal) . ", "; - } else { - $bindParam .= h($bindVal) . ", "; - } - } - $i['query'] .= " , params[ " . rtrim($bindParam, ', ') . " ]"; - } - printf('%s', - $k + 1, - h($i['query']), - $i['error'], - $i['affected'], - $i['numRows'], - $i['took'], - "\n" - ); - endforeach; - ?> -
(%s) %s %s took %s ms
NrQueryErrorAffectedNum. rowsTook (ms)
%d%s%s%d%d%d
- $logInfo): + $text = $logInfo['count'] > 1 ? 'queries' : 'query'; + printf( + '', + preg_replace('/[^A-Za-z0-9_]/', '_', uniqid(time(), true)) + ); + printf('', $source, $logInfo['count'], $text, $logInfo['time']); + ?> + + + + + + + + + + + + $i) : + $i += ['error' => '']; + if (!empty($i['params']) && is_array($i['params'])) { + $bindParam = $bindType = null; + if (preg_match('/.+ :.+/', $i['query'])) { + $bindType = true; + } + foreach ($i['params'] as $bindKey => $bindVal) { + if ($bindType === true) { + $bindParam .= h($bindKey) . " => " . h($bindVal) . ", "; + } else { + $bindParam .= h($bindVal) . ", "; + } + } + $i['query'] .= " , params[ " . rtrim($bindParam, ', ') . " ]"; + } + printf('%s', + $k + 1, + h($i['query']), + $i['error'], + $i['affected'], + $i['numRows'], + $i['took'], + "\n" + ); + endforeach; + ?> +
(%s) %s %s took %s ms
NrQueryErrorAffectedNum. rowsTook (ms)
%d%s%s%d%d%d
+ %s

', __d('cake_dev', 'Encountered unexpected %s. Cannot generate SQL log.', '$sqlLogs')); + printf('

%s

', __d('cake_dev', 'Encountered unexpected %s. Cannot generate SQL log.', '$sqlLogs')); endif; diff --git a/lib/Cake/View/Errors/fatal_error.ctp b/lib/Cake/View/Errors/fatal_error.ctp index 2b7c5a4a..2314c4dd 100755 --- a/lib/Cake/View/Errors/fatal_error.ctp +++ b/lib/Cake/View/Errors/fatal_error.ctp @@ -15,25 +15,25 @@ */ ?> -

-

- : - getMessage()); ?> -
+

+

+ : + getMessage()); ?> +
- : - getFile()); ?> -
+ : + getFile()); ?> +
- : - getLine()); ?> -

-

- : - -

+ : + getLine()); ?> +

+

+ : + +

\ No newline at end of file diff --git a/lib/Cake/View/Errors/missing_action.ctp b/lib/Cake/View/Errors/missing_action.ctp index 5582207f..7435bc40 100755 --- a/lib/Cake/View/Errors/missing_action.ctp +++ b/lib/Cake/View/Errors/missing_action.ctp @@ -15,15 +15,15 @@ */ ?> -

- : - ' . h($action) . '', '' . h($controller) . ''); ?> +

+ : + ' . h($action) . '', '' . h($controller) . ''); ?>

-

- : - ' . h($controller) . '::', '' . h($action) . '()', APP_DIR . DS . 'Controller' . DS . h($controller) . '.php'); ?> -

-
+    

+ : + ' . h($controller) . '::', '' . h($action) . '()', APP_DIR . DS . 'Controller' . DS . h($controller) . '.php'); ?> +

+
 <?php
 class  extends AppController {
 
@@ -34,10 +34,10 @@ class  extends AppController {
 
 }
 
-

- : - -

+

+ : + +

element('exception_stack_trace'); ?> \ No newline at end of file diff --git a/lib/Cake/View/Errors/missing_behavior.ctp b/lib/Cake/View/Errors/missing_behavior.ctp index 84d25a47..0cb078fb 100755 --- a/lib/Cake/View/Errors/missing_behavior.ctp +++ b/lib/Cake/View/Errors/missing_behavior.ctp @@ -16,25 +16,25 @@ $pluginDot = empty($plugin) ? null : $plugin . '.'; ?> -

-

- : - ' . h($pluginDot . $class) . ''); ?> -

-

- : - ' . h($class) . '', (empty($plugin) ? APP_DIR . DS : CakePlugin::path($plugin)) . 'Model' . DS . 'Behavior' . DS . h($class) . '.php'); ?> -

-
+    

+

+ : + ' . h($pluginDot . $class) . ''); ?> +

+

+ : + ' . h($class) . '', (empty($plugin) ? APP_DIR . DS : CakePlugin::path($plugin)) . 'Model' . DS . 'Behavior' . DS . h($class) . '.php'); ?> +

+
 <?php
 class  extends ModelBehavior {
 
 }
 
-

- : - -

+

+ : + +

element('exception_stack_trace'); diff --git a/lib/Cake/View/Errors/missing_component.ctp b/lib/Cake/View/Errors/missing_component.ctp index 6cc2cf2e..32eae0ae 100755 --- a/lib/Cake/View/Errors/missing_component.ctp +++ b/lib/Cake/View/Errors/missing_component.ctp @@ -18,12 +18,12 @@ $pluginDot = empty($plugin) ? null : $plugin . '.'; ?>

- : - ' . h($pluginDot . $class) . ''); ?> + : + ' . h($pluginDot . $class) . ''); ?>

- : - ' . h($class) . '', (empty($plugin) ? APP_DIR . DS : CakePlugin::path($plugin)) . 'Controller' . DS . 'Component' . DS . h($class) . '.php'); ?> + : + ' . h($class) . '', (empty($plugin) ? APP_DIR . DS : CakePlugin::path($plugin)) . 'Controller' . DS . 'Component' . DS . h($class) . '.php'); ?>

 <?php
@@ -32,8 +32,8 @@ class  extends Component {
 }
 

- : - + : +

-

-

- : - -
- -

+

+

+ : + +
+ +

-

- : - -

+

+ : + +

-

- : - -

+

+ : + +

element('exception_stack_trace'); diff --git a/lib/Cake/View/Errors/missing_controller.ctp b/lib/Cake/View/Errors/missing_controller.ctp index b23031fd..57dbf0af 100755 --- a/lib/Cake/View/Errors/missing_controller.ctp +++ b/lib/Cake/View/Errors/missing_controller.ctp @@ -18,12 +18,12 @@ $pluginDot = empty($plugin) ? null : $plugin . '.'; ?>

- : - ' . h($pluginDot . $class) . ''); ?> + : + ' . h($pluginDot . $class) . ''); ?>

- : - ' . h($class) . '', (empty($plugin) ? APP_DIR . DS : CakePlugin::path($plugin)) . 'Controller' . DS . h($class) . '.php'); ?> + : + ' . h($class) . '', (empty($plugin) ? APP_DIR . DS : CakePlugin::path($plugin)) . 'Controller' . DS . h($class) . '.php'); ?>

 <?php
@@ -32,8 +32,8 @@ class Ap
 }
 

- : - + : +

- : - + : +

- : - + : +

- : - + : +

- : - ' . h($pluginDot . $class) . ''); ?> - - - + : + ' . h($pluginDot . $class) . ''); ?> + + +

- : - + : +

- : - ' . h($config) . ''); ?> + : + ' . h($config) . ''); ?>

- : - + : +

- : - ' . h($pluginDot . $class) . ''); ?> + : + ' . h($pluginDot . $class) . ''); ?>

- : - ' . h($class) . '', (empty($plugin) ? APP_DIR . DS : CakePlugin::path($plugin)) . 'View' . DS . 'Helper' . DS . h($class) . '.php'); ?> + : + ' . h($class) . '', (empty($plugin) ? APP_DIR . DS : CakePlugin::path($plugin)) . 'View' . DS . 'Helper' . DS . h($class) . '.php'); ?>

 <?php
@@ -32,8 +32,8 @@ class  extends AppHelper {
 }
 

- : - + : +

- : - ' . h($file) . ''); ?> + : + ' . h($file) . ''); ?>

- - in one of the following paths: + + in one of the following paths:

    -_paths($this->plugin); - foreach ($paths as $path): - if (strpos($path, CORE_PATH) !== false) { - continue; - } - echo sprintf('
  • %s%s
  • ', h($path), h($file)); - endforeach; -?> + _paths($this->plugin); + foreach ($paths as $path): + if (strpos($path, CORE_PATH) !== false) { + continue; + } + echo sprintf('
  • %s%s
  • ', h($path), h($file)); + endforeach; + ?>

- : - + : +

- : - ' . h($plugin) . ''); ?> + : + ' . h($plugin) . ''); ?>

- : - + : +

 <?php
@@ -29,15 +29,15 @@ CakePlugin::load('');
 
 

- : - + : +

 CakePlugin::loadAll();
 

- : - + : +

- : - ' . h($table) . '', '' . h($class) . '', '' . h($ds) . ''); ?> + : + ' . h($table) . '', '' . h($class) . '', '' . h($ds) . ''); ?>

- : - + : +

- : - ' . h(Inflector::camelize($this->request->controller)) . 'Controller::', '' . h($this->request->action) . '()'); ?> + : + ' . h(Inflector::camelize($this->request->controller)) . 'Controller::', '' . h($this->request->action) . '()'); ?>

- - in one of the following paths: + + in one of the following paths:

    -_paths($this->plugin); - foreach ($paths as $path): - if (strpos($path, CORE_PATH) !== false) { - continue; - } - echo sprintf('
  • %s%s
  • ', h($path), h($file)); - endforeach; -?> + _paths($this->plugin); + foreach ($paths as $path): + if (strpos($path, CORE_PATH) !== false) { + continue; + } + echo sprintf('
  • %s%s
  • ', h($path), h($file)); + endforeach; + ?>

- : - + : +

- : - + : +

queryString)) : ?> -

- : - queryString); ?> -

+

+ : + queryString); ?> +

params)) : ?> - : - params); ?> + : + params); ?>

- : - + : +

element('exception_stack_trace'); diff --git a/lib/Cake/View/Errors/permissions_errors.ctp b/lib/Cake/View/Errors/permissions_errors.ctp index 97705fea..9920fee5 100755 --- a/lib/Cake/View/Errors/permissions_errors.ctp +++ b/lib/Cake/View/Errors/permissions_errors.ctp @@ -1,8 +1,9 @@
-
-

Permissions errors

-
- The folder where is the CMS need to be writable (755 or 775 permissions) for logs, updates and configurations. -
-
+
+

Permissions errors

+
+ The folder where is the CMS need to be writable (755 or 775 permissions) for logs, updates and + configurations. +
+
\ No newline at end of file diff --git a/lib/Cake/View/Errors/private_action.ctp b/lib/Cake/View/Errors/private_action.ctp index b7a025e5..fa7e8aa7 100755 --- a/lib/Cake/View/Errors/private_action.ctp +++ b/lib/Cake/View/Errors/private_action.ctp @@ -16,12 +16,12 @@ ?>

- : - ' . h($controller) . '::', '' . h($action) . '()'); ?> + : + ' . h($controller) . '::', '' . h($action) . '()'); ?>

- : - + : +

-

-

- : - -

-

- : - -

-
+    

+

+ : + +

+

+ : + +

+
 <?php
-function _scaffoldError() {
+function _scaffoldError() {
} @@ -33,6 +33,6 @@ function _scaffoldError() {
element('exception_stack_trace'); + echo $this->element('exception_stack_trace'); } ?> \ No newline at end of file diff --git a/lib/Cake/View/Helper.php b/lib/Cake/View/Helper.php index f8bdd2dd..64ac1a6a 100755 --- a/lib/Cake/View/Helper.php +++ b/lib/Cake/View/Helper.php @@ -24,964 +24,990 @@ * * @package Cake.View */ -class Helper extends CakeObject { - -/** - * Settings for this helper. - * - * @var array - */ - public $settings = array(); - -/** - * List of helpers used by this helper - * - * @var array - */ - public $helpers = array(); - -/** - * A helper lookup table used to lazy load helper objects. - * - * @var array - */ - protected $_helperMap = array(); - -/** - * The current theme name if any. - * - * @var string - */ - public $theme = null; - -/** - * Request object - * - * @var CakeRequest - */ - public $request = null; - -/** - * Plugin path - * - * @var string - */ - public $plugin = null; - -/** - * Holds the fields array('field_name' => array('type' => 'string', 'length' => 100), - * primaryKey and validates array('field_name') - * - * @var array - */ - public $fieldset = array(); - -/** - * Holds tag templates. - * - * @var array - */ - public $tags = array(); - -/** - * Holds the content to be cleaned. - * - * @var mixed - */ - protected $_tainted = null; - -/** - * Holds the cleaned content. - * - * @var mixed - */ - protected $_cleaned = null; - -/** - * The View instance this helper is attached to - * - * @var View - */ - protected $_View; - -/** - * A list of strings that should be treated as suffixes, or - * sub inputs for a parent input. This is used for date/time - * inputs primarily. - * - * @var array - */ - protected $_fieldSuffixes = array( - 'year', 'month', 'day', 'hour', 'min', 'second', 'meridian' - ); - -/** - * The name of the current model entities are in scope of. - * - * @see Helper::setEntity() - * @var string - */ - protected $_modelScope; - -/** - * The name of the current model association entities are in scope of. - * - * @see Helper::setEntity() - * @var string - */ - protected $_association; - -/** - * The dot separated list of elements the current field entity is for. - * - * @see Helper::setEntity() - * @var string - */ - protected $_entityPath; - -/** - * Minimized attributes - * - * @var array - */ - protected $_minimizedAttributes = array( - 'allowfullscreen', - 'async', - 'autofocus', - 'autoplay', - 'checked', - 'compact', - 'controls', - 'declare', - 'default', - 'defaultchecked', - 'defaultmuted', - 'defaultselected', - 'defer', - 'disabled', - 'enabled', - 'formnovalidate', - 'hidden', - 'indeterminate', - 'inert', - 'ismap', - 'itemscope', - 'loop', - 'multiple', - 'muted', - 'nohref', - 'noresize', - 'noshade', - 'novalidate', - 'nowrap', - 'open', - 'pauseonexit', - 'readonly', - 'required', - 'reversed', - 'scoped', - 'seamless', - 'selected', - 'sortable', - 'spellcheck', - 'truespeed', - 'typemustmatch', - 'visible' - ); - -/** - * Format to attribute - * - * @var string - */ - protected $_attributeFormat = '%s="%s"'; - -/** - * Format to attribute - * - * @var string - */ - protected $_minimizedAttributeFormat = '%s="%s"'; - -/** - * Default Constructor - * - * @param View $View The View this helper is being attached to. - * @param array $settings Configuration settings for the helper. - */ - public function __construct(View $View, $settings = array()) { - $this->_View = $View; - $this->request = $View->request; - if ($settings) { - $this->settings = Hash::merge($this->settings, $settings); - } - if (!empty($this->helpers)) { - $this->_helperMap = ObjectCollection::normalizeObjectArray($this->helpers); - } - } - -/** - * Provide non fatal errors on missing method calls. - * - * @param string $method Method to invoke - * @param array $params Array of params for the method. - * @return void - */ - public function __call($method, $params) { - trigger_error(__d('cake_dev', 'Method %1$s::%2$s does not exist', get_class($this), $method), E_USER_WARNING); - } - -/** - * Lazy loads helpers. Provides access to deprecated request properties as well. - * - * @param string $name Name of the property being accessed. - * @return mixed Helper or property found at $name - * @deprecated 3.0.0 Accessing request properties through this method is deprecated and will be removed in 3.0. - */ - public function __get($name) { - if (isset($this->_helperMap[$name]) && !isset($this->{$name})) { - $settings = array('enabled' => false) + (array)$this->_helperMap[$name]['settings']; - $this->{$name} = $this->_View->loadHelper($this->_helperMap[$name]['class'], $settings); - } - if (isset($this->{$name})) { - return $this->{$name}; - } - switch ($name) { - case 'base': - case 'here': - case 'webroot': - case 'data': - return $this->request->{$name}; - case 'action': - return isset($this->request->params['action']) ? $this->request->params['action'] : ''; - case 'params': - return $this->request; - } - } - -/** - * Provides backwards compatibility access for setting values to the request object. - * - * @param string $name Name of the property being accessed. - * @param mixed $value Value to set. - * @return void - * @deprecated 3.0.0 This method will be removed in 3.0 - */ - public function __set($name, $value) { - switch ($name) { - case 'base': - case 'here': - case 'webroot': - case 'data': - $this->request->{$name} = $value; - return; - case 'action': - $this->request->params['action'] = $value; - return; - } - $this->{$name} = $value; - } - -/** - * Finds URL for specified action. - * - * Returns a URL pointing at the provided parameters. - * - * @param string|array $url Either a relative string url like `/products/view/23` or - * an array of URL parameters. Using an array for URLs will allow you to leverage - * the reverse routing features of CakePHP. - * @param bool $full If true, the full base URL will be prepended to the result - * @return string Full translated URL with base path. - * @link https://book.cakephp.org/2.0/en/views/helpers.html - */ - public function url($url = null, $full = false) { - return h(Router::url($url, $full)); - } - -/** - * Checks if a file exists when theme is used, if no file is found default location is returned - * - * @param string $file The file to create a webroot path to. - * @return string Web accessible path to file. - */ - public function webroot($file) { - $asset = explode('?', $file); - $asset[1] = isset($asset[1]) ? '?' . $asset[1] : null; - $webPath = "{$this->request->webroot}" . $asset[0]; - $file = $asset[0]; - - if (!empty($this->theme)) { - $file = trim($file, '/'); - $theme = $this->theme . '/'; - - if (DS === '\\') { - $file = str_replace('/', '\\', $file); - } - - if (file_exists(Configure::read('App.www_root') . 'theme' . DS . $this->theme . DS . $file)) { - $webPath = "{$this->request->webroot}theme/" . $theme . $asset[0]; - } else { - $themePath = App::themePath($this->theme); - $path = $themePath . 'webroot' . DS . $file; - if (file_exists($path)) { - $webPath = "{$this->request->webroot}theme/" . $theme . $asset[0]; - } - } - } - if (strpos($webPath, '//') !== false) { - return str_replace('//', '/', $webPath . $asset[1]); - } - return $webPath . $asset[1]; - } - -/** - * Generate URL for given asset file. Depending on options passed provides full URL with domain name. - * Also calls Helper::assetTimestamp() to add timestamp to local files - * - * @param string|array $path Path string or URL array - * @param array $options Options array. Possible keys: - * `fullBase` Return full URL with domain name - * `pathPrefix` Path prefix for relative URLs - * `ext` Asset extension to append - * `plugin` False value will prevent parsing path as a plugin - * @return string Generated URL - */ - public function assetUrl($path, $options = array()) { - if (is_array($path)) { - return $this->url($path, !empty($options['fullBase'])); - } - if (strpos($path, '://') !== false) { - return $path; - } - if (!array_key_exists('plugin', $options) || $options['plugin'] !== false) { - list($plugin, $path) = $this->_View->pluginSplit($path, false); - } - if (!empty($options['pathPrefix']) && $path[0] !== '/') { - $path = $options['pathPrefix'] . $path; - } - if (!empty($options['ext']) && - strpos($path, '?') === false && - substr($path, -strlen($options['ext'])) !== $options['ext'] - ) { - $path .= $options['ext']; - } - if (preg_match('|^([a-z0-9]+:)?//|', $path)) { - return $path; - } - if (isset($plugin)) { - $path = Inflector::underscore($plugin) . '/' . $path; - } - $path = $this->_encodeUrl($this->assetTimestamp($this->webroot($path))); - - if (!empty($options['fullBase'])) { - $path = rtrim(Router::fullBaseUrl(), '/') . '/' . ltrim($path, '/'); - } - return $path; - } - -/** - * Encodes a URL for use in HTML attributes. - * - * @param string $url The URL to encode. - * @return string The URL encoded for both URL & HTML contexts. - */ - protected function _encodeUrl($url) { - $path = parse_url($url, PHP_URL_PATH); - $parts = array_map('rawurldecode', explode('/', $path)); - $parts = array_map('rawurlencode', $parts); - $encoded = implode('/', $parts); - return h(str_replace($path, $encoded, $url)); - } - -/** - * Adds a timestamp to a file based resource based on the value of `Asset.timestamp` in - * Configure. If Asset.timestamp is true and debug > 0, or Asset.timestamp === 'force' - * a timestamp will be added. - * - * @param string $path The file path to timestamp, the path must be inside WWW_ROOT - * @return string Path with a timestamp added, or not. - */ - public function assetTimestamp($path) { - $stamp = Configure::read('Asset.timestamp'); - $timestampEnabled = $stamp === 'force' || ($stamp === true && Configure::read('debug') > 0); - if ($timestampEnabled && strpos($path, '?') === false) { - $filepath = preg_replace( - '/^' . preg_quote($this->request->webroot, '/') . '/', - '', - urldecode($path) - ); - $webrootPath = WWW_ROOT . str_replace('/', DS, $filepath); - if (file_exists($webrootPath)) { - //@codingStandardsIgnoreStart - return $path . '?' . @filemtime($webrootPath); - //@codingStandardsIgnoreEnd - } - $segments = explode('/', ltrim($filepath, '/')); - if ($segments[0] === 'theme') { - $theme = $segments[1]; - unset($segments[0], $segments[1]); - $themePath = App::themePath($theme) . 'webroot' . DS . implode(DS, $segments); - //@codingStandardsIgnoreStart - return $path . '?' . @filemtime($themePath); - //@codingStandardsIgnoreEnd - } else { - $plugin = Inflector::camelize($segments[0]); - if (CakePlugin::loaded($plugin)) { - unset($segments[0]); - $pluginPath = CakePlugin::path($plugin) . 'webroot' . DS . implode(DS, $segments); - //@codingStandardsIgnoreStart - return $path . '?' . @filemtime($pluginPath); - //@codingStandardsIgnoreEnd - } - } - } - return $path; - } - -/** - * Used to remove harmful tags from content. Removes a number of well known XSS attacks - * from content. However, is not guaranteed to remove all possibilities. Escaping - * content is the best way to prevent all possible attacks. - * - * @param string|array $output Either an array of strings to clean or a single string to clean. - * @return string|array|null Cleaned content for output - * @deprecated 3.0.0 This method will be removed in 3.0 - */ - public function clean($output) { - $this->_reset(); - if (empty($output)) { - return null; - } - if (is_array($output)) { - foreach ($output as $key => $value) { - $return[$key] = $this->clean($value); - } - return $return; - } - $this->_tainted = $output; - $this->_clean(); - return $this->_cleaned; - } - -/** - * Returns a space-delimited string with items of the $options array. If a key - * of $options array happens to be one of those listed in `Helper::$_minimizedAttributes` - * - * And its value is one of: - * - * - '1' (string) - * - 1 (integer) - * - true (boolean) - * - 'true' (string) - * - * Then the value will be reset to be identical with key's name. - * If the value is not one of these 3, the parameter is not output. - * - * 'escape' is a special option in that it controls the conversion of - * attributes to their html-entity encoded equivalents. Set to false to disable html-encoding. - * - * If value for any option key is set to `null` or `false`, that option will be excluded from output. - * - * @param array $options Array of options. - * @param array $exclude Array of options to be excluded, the options here will not be part of the return. - * @param string $insertBefore String to be inserted before options. - * @param string $insertAfter String to be inserted after options. - * @return string Composed attributes. - * @deprecated 3.0.0 This method will be moved to HtmlHelper in 3.0 - */ - protected function _parseAttributes($options, $exclude = null, $insertBefore = ' ', $insertAfter = null) { - if (!is_string($options)) { - $options = (array)$options + array('escape' => true); - - if (!is_array($exclude)) { - $exclude = array(); - } - - $exclude = array('escape' => true) + array_flip($exclude); - $escape = $options['escape']; - $attributes = array(); - - foreach ($options as $key => $value) { - if (!isset($exclude[$key]) && $value !== false && $value !== null) { - $attributes[] = $this->_formatAttribute($key, $value, $escape); - } - } - $out = implode(' ', $attributes); - } else { - $out = $options; - } - return $out ? $insertBefore . $out . $insertAfter : ''; - } - -/** - * Formats an individual attribute, and returns the string value of the composed attribute. - * Works with minimized attributes that have the same value as their name such as 'disabled' and 'checked' - * - * @param string $key The name of the attribute to create - * @param string $value The value of the attribute to create. - * @param bool $escape Define if the value must be escaped - * @return string The composed attribute. - * @deprecated 3.0.0 This method will be moved to HtmlHelper in 3.0 - */ - protected function _formatAttribute($key, $value, $escape = true) { - if (is_array($value)) { - $value = implode(' ', $value); - } - if (is_numeric($key)) { - return sprintf($this->_minimizedAttributeFormat, $value, $value); - } - $truthy = array(1, '1', true, 'true', $key); - $isMinimized = in_array($key, $this->_minimizedAttributes); - if ($isMinimized && in_array($value, $truthy, true)) { - return sprintf($this->_minimizedAttributeFormat, $key, $key); - } - if ($isMinimized) { - return ''; - } - return sprintf($this->_attributeFormat, $key, ($escape ? h($value) : $value)); - } - -/** - * Returns a string to be used as onclick handler for confirm dialogs. - * - * @param string $message Message to be displayed - * @param string $okCode Code to be executed after user chose 'OK' - * @param string $cancelCode Code to be executed after user chose 'Cancel', also executed when okCode doesn't return - * @param array $options Array of options - * @return string onclick JS code - */ - protected function _confirm($message, $okCode, $cancelCode = '', $options = array()) { - $message = json_encode($message); - $confirm = "if (confirm({$message})) { {$okCode} } {$cancelCode}"; - if (isset($options['escape']) && $options['escape'] === false) { - $confirm = h($confirm); - } - return $confirm; - } - -/** - * Sets this helper's model and field properties to the dot-separated value-pair in $entity. - * - * @param string $entity A field name, like "ModelName.fieldName" or "ModelName.ID.fieldName" - * @param bool $setScope Sets the view scope to the model specified in $tagValue - * @return void - */ - public function setEntity($entity, $setScope = false) { - if ($entity === null) { - $this->_modelScope = false; - } - if ($setScope === true) { - $this->_modelScope = $entity; - } - $parts = array_values(Hash::filter(explode('.', $entity))); - if (empty($parts)) { - return; - } - $count = count($parts); - $lastPart = isset($parts[$count - 1]) ? $parts[$count - 1] : null; - - // Either 'body' or 'date.month' type inputs. - if (($count === 1 && $this->_modelScope && !$setScope) || - ( - $count === 2 && - in_array($lastPart, $this->_fieldSuffixes) && - $this->_modelScope && - $parts[0] !== $this->_modelScope - ) - ) { - $entity = $this->_modelScope . '.' . $entity; - } - - // 0.name, 0.created.month style inputs. Excludes inputs with the modelScope in them. - if ($count >= 2 && - is_numeric($parts[0]) && - !is_numeric($parts[1]) && - $this->_modelScope && - strpos($entity, $this->_modelScope) === false - ) { - $entity = $this->_modelScope . '.' . $entity; - } - - $this->_association = null; - - $isHabtm = ( - isset($this->fieldset[$this->_modelScope]['fields'][$parts[0]]['type']) && - $this->fieldset[$this->_modelScope]['fields'][$parts[0]]['type'] === 'multiple' - ); - - // habtm models are special - if ($count === 1 && $isHabtm) { - $this->_association = $parts[0]; - $entity = $parts[0] . '.' . $parts[0]; - } else { - // check for associated model. - $reversed = array_reverse($parts); - foreach ($reversed as $i => $part) { - if ($i > 0 && preg_match('/^[A-Z]/', $part)) { - $this->_association = $part; - break; - } - } - } - $this->_entityPath = $entity; - } - -/** - * Returns the entity reference of the current context as an array of identity parts - * - * @return array An array containing the identity elements of an entity - */ - public function entity() { - return explode('.', $this->_entityPath); - } - -/** - * Gets the currently-used model of the rendering context. - * - * @return string - */ - public function model() { - if ($this->_association) { - return $this->_association; - } - return $this->_modelScope; - } - -/** - * Gets the currently-used model field of the rendering context. - * Strips off field suffixes such as year, month, day, hour, min, meridian - * when the current entity is longer than 2 elements. - * - * @return string - */ - public function field() { - $entity = $this->entity(); - $count = count($entity); - $last = $entity[$count - 1]; - if ($count > 2 && in_array($last, $this->_fieldSuffixes)) { - $last = isset($entity[$count - 2]) ? $entity[$count - 2] : null; - } - return $last; - } - -/** - * Generates a DOM ID for the selected element, if one is not set. - * Uses the current View::entity() settings to generate a CamelCased id attribute. - * - * @param array|string $options Either an array of html attributes to add $id into, or a string - * with a view entity path to get a domId for. - * @param string $id The name of the 'id' attribute. - * @return mixed If $options was an array, an array will be returned with $id set. If a string - * was supplied, a string will be returned. - */ - public function domId($options = null, $id = 'id') { - if (is_array($options) && array_key_exists($id, $options) && $options[$id] === null) { - unset($options[$id]); - return $options; - } elseif (!is_array($options) && $options !== null) { - $this->setEntity($options); - return $this->domId(); - } - - $entity = $this->entity(); - $model = array_shift($entity); - $dom = $model . implode('', array_map(array('Inflector', 'camelize'), $entity)); - - if (is_array($options) && !array_key_exists($id, $options)) { - $options[$id] = $dom; - } elseif ($options === null) { - return $dom; - } - return $options; - } - -/** - * Gets the input field name for the current tag. Creates input name attributes - * using CakePHP's data[Model][field] formatting. - * - * @param array|string $options If an array, should be an array of attributes that $key needs to be added to. - * If a string or null, will be used as the View entity. - * @param string $field Field name. - * @param string $key The name of the attribute to be set, defaults to 'name' - * @return mixed If an array was given for $options, an array with $key set will be returned. - * If a string was supplied a string will be returned. - */ - protected function _name($options = array(), $field = null, $key = 'name') { - if ($options === null) { - $options = array(); - } elseif (is_string($options)) { - $field = $options; - $options = 0; - } - - if (!empty($field)) { - $this->setEntity($field); - } - - if (is_array($options) && array_key_exists($key, $options)) { - return $options; - } - - switch ($field) { - case '_method': - $name = $field; - break; - default: - $name = 'data[' . implode('][', $this->entity()) . ']'; - } - - if (is_array($options)) { - $options[$key] = $name; - return $options; - } - return $name; - } - -/** - * Gets the data for the current tag - * - * @param array|string $options If an array, should be an array of attributes that $key needs to be added to. - * If a string or null, will be used as the View entity. - * @param string $field Field name. - * @param string $key The name of the attribute to be set, defaults to 'value' - * @return mixed If an array was given for $options, an array with $key set will be returned. - * If a string was supplied a string will be returned. - */ - public function value($options = array(), $field = null, $key = 'value') { - if ($options === null) { - $options = array(); - } elseif (is_string($options)) { - $field = $options; - $options = 0; - } - - if (is_array($options) && isset($options[$key])) { - return $options; - } - - if (!empty($field)) { - $this->setEntity($field); - } - $result = null; - $data = $this->request->data; - - $entity = $this->entity(); - if (!empty($data) && is_array($data) && !empty($entity)) { - $result = Hash::get($data, implode('.', $entity)); - } - - $habtmKey = $this->field(); - if (empty($result) && isset($data[$habtmKey][$habtmKey]) && is_array($data[$habtmKey])) { - $result = $data[$habtmKey][$habtmKey]; - } elseif (empty($result) && isset($data[$habtmKey]) && is_array($data[$habtmKey])) { - if (ClassRegistry::isKeySet($habtmKey)) { - $model = ClassRegistry::getObject($habtmKey); - $result = $this->_selectedArray($data[$habtmKey], $model->primaryKey); - } - } - - if (is_array($options)) { - if ($result === null && isset($options['default'])) { - $result = $options['default']; - } - unset($options['default']); - } - - if (is_array($options)) { - $options[$key] = $result; - return $options; - } - return $result; - } - -/** - * Sets the defaults for an input tag. Will set the - * name, value, and id attributes for an array of html attributes. - * - * @param string $field The field name to initialize. - * @param array $options Array of options to use while initializing an input field. - * @return array Array options for the form input. - */ - protected function _initInputField($field, $options = array()) { - if ($field !== null) { - $this->setEntity($field); - } - $options = (array)$options; - $options = $this->_name($options); - $options = $this->value($options); - $options = $this->domId($options); - return $options; - } - -/** - * Adds the given class to the element options - * - * @param array $options Array options/attributes to add a class to - * @param string $class The class name being added. - * @param string $key the key to use for class. - * @return array Array of options with $key set. - */ - public function addClass($options = array(), $class = null, $key = 'class') { - if (isset($options[$key]) && trim($options[$key])) { - $options[$key] .= ' ' . $class; - } else { - $options[$key] = $class; - } - return $options; - } - -/** - * Returns a string generated by a helper method - * - * This method can be overridden in subclasses to do generalized output post-processing - * - * @param string $str String to be output. - * @return string - * @deprecated 3.0.0 This method will be removed in future versions. - */ - public function output($str) { - return $str; - } - -/** - * Before render callback. beforeRender is called before the view file is rendered. - * - * Overridden in subclasses. - * - * @param string $viewFile The view file that is going to be rendered - * @return void - */ - public function beforeRender($viewFile) { - } - -/** - * After render callback. afterRender is called after the view file is rendered - * but before the layout has been rendered. - * - * Overridden in subclasses. - * - * @param string $viewFile The view file that was rendered. - * @return void - */ - public function afterRender($viewFile) { - } - -/** - * Before layout callback. beforeLayout is called before the layout is rendered. - * - * Overridden in subclasses. - * - * @param string $layoutFile The layout about to be rendered. - * @return void - */ - public function beforeLayout($layoutFile) { - } - -/** - * After layout callback. afterLayout is called after the layout has rendered. - * - * Overridden in subclasses. - * - * @param string $layoutFile The layout file that was rendered. - * @return void - */ - public function afterLayout($layoutFile) { - } - -/** - * Before render file callback. - * Called before any view fragment is rendered. - * - * Overridden in subclasses. - * - * @param string $viewFile The file about to be rendered. - * @return void - */ - public function beforeRenderFile($viewFile) { - } - -/** - * After render file callback. - * Called after any view fragment is rendered. - * - * Overridden in subclasses. - * - * @param string $viewFile The file just be rendered. - * @param string $content The content that was rendered. - * @return void - */ - public function afterRenderFile($viewFile, $content) { - } - -/** - * Transforms a recordset from a hasAndBelongsToMany association to a list of selected - * options for a multiple select element - * - * @param string|array $data Data array or model name. - * @param string $key Field name. - * @return array - */ - protected function _selectedArray($data, $key = 'id') { - if (!is_array($data)) { - $model = $data; - if (!empty($this->request->data[$model][$model])) { - return $this->request->data[$model][$model]; - } - if (!empty($this->request->data[$model])) { - $data = $this->request->data[$model]; - } - } - $array = array(); - if (!empty($data)) { - foreach ($data as $row) { - if (isset($row[$key])) { - $array[$row[$key]] = $row[$key]; - } - } - } - return empty($array) ? null : $array; - } - -/** - * Resets the vars used by Helper::clean() to null - * - * @return void - */ - protected function _reset() { - $this->_tainted = null; - $this->_cleaned = null; - } - -/** - * Removes harmful content from output - * - * @return void - */ - protected function _clean() { - if (get_magic_quotes_gpc()) { - $this->_cleaned = stripslashes($this->_tainted); - } else { - $this->_cleaned = $this->_tainted; - } - - $this->_cleaned = str_replace(array("&", "<", ">"), array("&amp;", "&lt;", "&gt;"), $this->_cleaned); - $this->_cleaned = preg_replace('#(&\#*\w+)[\x00-\x20]+;#u', "$1;", $this->_cleaned); - $this->_cleaned = preg_replace('#(&\#x*)([0-9A-F]+);*#iu', "$1$2;", $this->_cleaned); - $this->_cleaned = html_entity_decode($this->_cleaned, ENT_COMPAT, "UTF-8"); - $this->_cleaned = preg_replace('#(<[^>]+[\x00-\x20\"\'\/])(on|xmlns)[^>]*>#iUu', "$1>", $this->_cleaned); - $this->_cleaned = preg_replace('#([a-z]*)[\x00-\x20]*=[\x00-\x20]*([\`\'\"]*)[\\x00-\x20]*j[\x00-\x20]*a[\x00-\x20]*v[\x00-\x20]*a[\x00-\x20]*s[\x00-\x20]*c[\x00-\x20]*r[\x00-\x20]*i[\x00-\x20]*p[\x00-\x20]*t[\x00-\x20]*:#iUu', '$1=$2nojavascript...', $this->_cleaned); - $this->_cleaned = preg_replace('#([a-z]*)[\x00-\x20]*=([\'\"]*)[\x00-\x20]*v[\x00-\x20]*b[\x00-\x20]*s[\x00-\x20]*c[\x00-\x20]*r[\x00-\x20]*i[\x00-\x20]*p[\x00-\x20]*t[\x00-\x20]*:#iUu', '$1=$2novbscript...', $this->_cleaned); - $this->_cleaned = preg_replace('#([a-z]*)[\x00-\x20]*=*([\'\"]*)[\x00-\x20]*-moz-binding[\x00-\x20]*:#iUu', '$1=$2nomozbinding...', $this->_cleaned); - $this->_cleaned = preg_replace('#([a-z]*)[\x00-\x20]*=([\'\"]*)[\x00-\x20]*data[\x00-\x20]*:#Uu', '$1=$2nodata...', $this->_cleaned); - $this->_cleaned = preg_replace('#(<[^>]+)style[\x00-\x20]*=[\x00-\x20]*([\`\'\"]*).*expression[\x00-\x20]*\([^>]*>#iU', "$1>", $this->_cleaned); - $this->_cleaned = preg_replace('#(<[^>]+)style[\x00-\x20]*=[\x00-\x20]*([\`\'\"]*).*behaviour[\x00-\x20]*\([^>]*>#iU', "$1>", $this->_cleaned); - $this->_cleaned = preg_replace('#(<[^>]+)style[\x00-\x20]*=[\x00-\x20]*([\`\'\"]*).*s[\x00-\x20]*c[\x00-\x20]*r[\x00-\x20]*i[\x00-\x20]*p[\x00-\x20]*t[\x00-\x20]*:*[^>]*>#iUu', "$1>", $this->_cleaned); - $this->_cleaned = preg_replace('#]*>#i', "", $this->_cleaned); - do { - $oldstring = $this->_cleaned; - $this->_cleaned = preg_replace('#]*>#i', "", $this->_cleaned); - } while ($oldstring !== $this->_cleaned); - $this->_cleaned = str_replace(array("&", "<", ">"), array("&amp;", "&lt;", "&gt;"), $this->_cleaned); - } +class Helper extends CakeObject +{ + + /** + * Settings for this helper. + * + * @var array + */ + public $settings = []; + + /** + * List of helpers used by this helper + * + * @var array + */ + public $helpers = []; + /** + * The current theme name if any. + * + * @var string + */ + public $theme = null; + /** + * Request object + * + * @var CakeRequest + */ + public $request = null; + /** + * Plugin path + * + * @var string + */ + public $plugin = null; + /** + * Holds the fields array('field_name' => array('type' => 'string', 'length' => 100), + * primaryKey and validates array('field_name') + * + * @var array + */ + public $fieldset = []; + /** + * Holds tag templates. + * + * @var array + */ + public $tags = []; + /** + * A helper lookup table used to lazy load helper objects. + * + * @var array + */ + protected $_helperMap = []; + /** + * Holds the content to be cleaned. + * + * @var mixed + */ + protected $_tainted = null; + + /** + * Holds the cleaned content. + * + * @var mixed + */ + protected $_cleaned = null; + + /** + * The View instance this helper is attached to + * + * @var View + */ + protected $_View; + + /** + * A list of strings that should be treated as suffixes, or + * sub inputs for a parent input. This is used for date/time + * inputs primarily. + * + * @var array + */ + protected $_fieldSuffixes = [ + 'year', 'month', 'day', 'hour', 'min', 'second', 'meridian' + ]; + + /** + * The name of the current model entities are in scope of. + * + * @see Helper::setEntity() + * @var string + */ + protected $_modelScope; + + /** + * The name of the current model association entities are in scope of. + * + * @see Helper::setEntity() + * @var string + */ + protected $_association; + + /** + * The dot separated list of elements the current field entity is for. + * + * @see Helper::setEntity() + * @var string + */ + protected $_entityPath; + + /** + * Minimized attributes + * + * @var array + */ + protected $_minimizedAttributes = [ + 'allowfullscreen', + 'async', + 'autofocus', + 'autoplay', + 'checked', + 'compact', + 'controls', + 'declare', + 'default', + 'defaultchecked', + 'defaultmuted', + 'defaultselected', + 'defer', + 'disabled', + 'enabled', + 'formnovalidate', + 'hidden', + 'indeterminate', + 'inert', + 'ismap', + 'itemscope', + 'loop', + 'multiple', + 'muted', + 'nohref', + 'noresize', + 'noshade', + 'novalidate', + 'nowrap', + 'open', + 'pauseonexit', + 'readonly', + 'required', + 'reversed', + 'scoped', + 'seamless', + 'selected', + 'sortable', + 'spellcheck', + 'truespeed', + 'typemustmatch', + 'visible' + ]; + + /** + * Format to attribute + * + * @var string + */ + protected $_attributeFormat = '%s="%s"'; + + /** + * Format to attribute + * + * @var string + */ + protected $_minimizedAttributeFormat = '%s="%s"'; + + /** + * Default Constructor + * + * @param View $View The View this helper is being attached to. + * @param array $settings Configuration settings for the helper. + */ + public function __construct(View $View, $settings = []) + { + $this->_View = $View; + $this->request = $View->request; + if ($settings) { + $this->settings = Hash::merge($this->settings, $settings); + } + if (!empty($this->helpers)) { + $this->_helperMap = ObjectCollection::normalizeObjectArray($this->helpers); + } + } + + /** + * Provide non fatal errors on missing method calls. + * + * @param string $method Method to invoke + * @param array $params Array of params for the method. + * @return void + */ + public function __call($method, $params) + { + trigger_error(__d('cake_dev', 'Method %1$s::%2$s does not exist', get_class($this), $method), E_USER_WARNING); + } + + /** + * Lazy loads helpers. Provides access to deprecated request properties as well. + * + * @param string $name Name of the property being accessed. + * @return mixed Helper or property found at $name + * @deprecated 3.0.0 Accessing request properties through this method is deprecated and will be removed in 3.0. + */ + public function __get($name) + { + if (isset($this->_helperMap[$name]) && !isset($this->{$name})) { + $settings = ['enabled' => false] + (array)$this->_helperMap[$name]['settings']; + $this->{$name} = $this->_View->loadHelper($this->_helperMap[$name]['class'], $settings); + } + if (isset($this->{$name})) { + return $this->{$name}; + } + switch ($name) { + case 'base': + case 'here': + case 'webroot': + case 'data': + return $this->request->{$name}; + case 'action': + return isset($this->request->params['action']) ? $this->request->params['action'] : ''; + case 'params': + return $this->request; + } + } + + /** + * Provides backwards compatibility access for setting values to the request object. + * + * @param string $name Name of the property being accessed. + * @param mixed $value Value to set. + * @return void + * @deprecated 3.0.0 This method will be removed in 3.0 + */ + public function __set($name, $value) + { + switch ($name) { + case 'base': + case 'here': + case 'webroot': + case 'data': + $this->request->{$name} = $value; + return; + case 'action': + $this->request->params['action'] = $value; + return; + } + $this->{$name} = $value; + } + + /** + * Generate URL for given asset file. Depending on options passed provides full URL with domain name. + * Also calls Helper::assetTimestamp() to add timestamp to local files + * + * @param string|array $path Path string or URL array + * @param array $options Options array. Possible keys: + * `fullBase` Return full URL with domain name + * `pathPrefix` Path prefix for relative URLs + * `ext` Asset extension to append + * `plugin` False value will prevent parsing path as a plugin + * @return string Generated URL + */ + public function assetUrl($path, $options = []) + { + if (is_array($path)) { + return $this->url($path, !empty($options['fullBase'])); + } + if (strpos($path, '://') !== false) { + return $path; + } + if (!array_key_exists('plugin', $options) || $options['plugin'] !== false) { + list($plugin, $path) = $this->_View->pluginSplit($path, false); + } + if (!empty($options['pathPrefix']) && $path[0] !== '/') { + $path = $options['pathPrefix'] . $path; + } + if (!empty($options['ext']) && + strpos($path, '?') === false && + substr($path, -strlen($options['ext'])) !== $options['ext'] + ) { + $path .= $options['ext']; + } + if (preg_match('|^([a-z0-9]+:)?//|', $path)) { + return $path; + } + if (isset($plugin)) { + $path = Inflector::underscore($plugin) . '/' . $path; + } + $path = $this->_encodeUrl($this->assetTimestamp($this->webroot($path))); + + if (!empty($options['fullBase'])) { + $path = rtrim(Router::fullBaseUrl(), '/') . '/' . ltrim($path, '/'); + } + return $path; + } + + /** + * Finds URL for specified action. + * + * Returns a URL pointing at the provided parameters. + * + * @param string|array $url Either a relative string url like `/products/view/23` or + * an array of URL parameters. Using an array for URLs will allow you to leverage + * the reverse routing features of CakePHP. + * @param bool $full If true, the full base URL will be prepended to the result + * @return string Full translated URL with base path. + * @link https://book.cakephp.org/2.0/en/views/helpers.html + */ + public function url($url = null, $full = false) + { + return h(Router::url($url, $full)); + } + + /** + * Encodes a URL for use in HTML attributes. + * + * @param string $url The URL to encode. + * @return string The URL encoded for both URL & HTML contexts. + */ + protected function _encodeUrl($url) + { + $path = parse_url($url, PHP_URL_PATH); + $parts = array_map('rawurldecode', explode('/', $path)); + $parts = array_map('rawurlencode', $parts); + $encoded = implode('/', $parts); + return h(str_replace($path, $encoded, $url)); + } + + /** + * Adds a timestamp to a file based resource based on the value of `Asset.timestamp` in + * Configure. If Asset.timestamp is true and debug > 0, or Asset.timestamp === 'force' + * a timestamp will be added. + * + * @param string $path The file path to timestamp, the path must be inside WWW_ROOT + * @return string Path with a timestamp added, or not. + */ + public function assetTimestamp($path) + { + $stamp = Configure::read('Asset.timestamp'); + $timestampEnabled = $stamp === 'force' || ($stamp === true && Configure::read('debug') > 0); + if ($timestampEnabled && strpos($path, '?') === false) { + $filepath = preg_replace( + '/^' . preg_quote($this->request->webroot, '/') . '/', + '', + urldecode($path) + ); + $webrootPath = WWW_ROOT . str_replace('/', DS, $filepath); + if (file_exists($webrootPath)) { + //@codingStandardsIgnoreStart + return $path . '?' . @filemtime($webrootPath); + //@codingStandardsIgnoreEnd + } + $segments = explode('/', ltrim($filepath, '/')); + if ($segments[0] === 'theme') { + $theme = $segments[1]; + unset($segments[0], $segments[1]); + $themePath = App::themePath($theme) . 'webroot' . DS . implode(DS, $segments); + //@codingStandardsIgnoreStart + return $path . '?' . @filemtime($themePath); + //@codingStandardsIgnoreEnd + } else { + $plugin = Inflector::camelize($segments[0]); + if (CakePlugin::loaded($plugin)) { + unset($segments[0]); + $pluginPath = CakePlugin::path($plugin) . 'webroot' . DS . implode(DS, $segments); + //@codingStandardsIgnoreStart + return $path . '?' . @filemtime($pluginPath); + //@codingStandardsIgnoreEnd + } + } + } + return $path; + } + + /** + * Checks if a file exists when theme is used, if no file is found default location is returned + * + * @param string $file The file to create a webroot path to. + * @return string Web accessible path to file. + */ + public function webroot($file) + { + $asset = explode('?', $file); + $asset[1] = isset($asset[1]) ? '?' . $asset[1] : null; + $webPath = "{$this->request->webroot}" . $asset[0]; + $file = $asset[0]; + + if (!empty($this->theme)) { + $file = trim($file, '/'); + $theme = $this->theme . '/'; + + if (DS === '\\') { + $file = str_replace('/', '\\', $file); + } + + if (file_exists(Configure::read('App.www_root') . 'theme' . DS . $this->theme . DS . $file)) { + $webPath = "{$this->request->webroot}theme/" . $theme . $asset[0]; + } else { + $themePath = App::themePath($this->theme); + $path = $themePath . 'webroot' . DS . $file; + if (file_exists($path)) { + $webPath = "{$this->request->webroot}theme/" . $theme . $asset[0]; + } + } + } + if (strpos($webPath, '//') !== false) { + return str_replace('//', '/', $webPath . $asset[1]); + } + return $webPath . $asset[1]; + } + + /** + * Used to remove harmful tags from content. Removes a number of well known XSS attacks + * from content. However, is not guaranteed to remove all possibilities. Escaping + * content is the best way to prevent all possible attacks. + * + * @param string|array $output Either an array of strings to clean or a single string to clean. + * @return string|array|null Cleaned content for output + * @deprecated 3.0.0 This method will be removed in 3.0 + */ + public function clean($output) + { + $this->_reset(); + if (empty($output)) { + return null; + } + if (is_array($output)) { + foreach ($output as $key => $value) { + $return[$key] = $this->clean($value); + } + return $return; + } + $this->_tainted = $output; + $this->_clean(); + return $this->_cleaned; + } + + /** + * Resets the vars used by Helper::clean() to null + * + * @return void + */ + protected function _reset() + { + $this->_tainted = null; + $this->_cleaned = null; + } + + /** + * Removes harmful content from output + * + * @return void + */ + protected function _clean() + { + if (get_magic_quotes_gpc()) { + $this->_cleaned = stripslashes($this->_tainted); + } else { + $this->_cleaned = $this->_tainted; + } + + $this->_cleaned = str_replace(["&", "<", ">"], ["&amp;", "&lt;", "&gt;"], $this->_cleaned); + $this->_cleaned = preg_replace('#(&\#*\w+)[\x00-\x20]+;#u', "$1;", $this->_cleaned); + $this->_cleaned = preg_replace('#(&\#x*)([0-9A-F]+);*#iu', "$1$2;", $this->_cleaned); + $this->_cleaned = html_entity_decode($this->_cleaned, ENT_COMPAT, "UTF-8"); + $this->_cleaned = preg_replace('#(<[^>]+[\x00-\x20\"\'\/])(on|xmlns)[^>]*>#iUu', "$1>", $this->_cleaned); + $this->_cleaned = preg_replace('#([a-z]*)[\x00-\x20]*=[\x00-\x20]*([\`\'\"]*)[\\x00-\x20]*j[\x00-\x20]*a[\x00-\x20]*v[\x00-\x20]*a[\x00-\x20]*s[\x00-\x20]*c[\x00-\x20]*r[\x00-\x20]*i[\x00-\x20]*p[\x00-\x20]*t[\x00-\x20]*:#iUu', '$1=$2nojavascript...', $this->_cleaned); + $this->_cleaned = preg_replace('#([a-z]*)[\x00-\x20]*=([\'\"]*)[\x00-\x20]*v[\x00-\x20]*b[\x00-\x20]*s[\x00-\x20]*c[\x00-\x20]*r[\x00-\x20]*i[\x00-\x20]*p[\x00-\x20]*t[\x00-\x20]*:#iUu', '$1=$2novbscript...', $this->_cleaned); + $this->_cleaned = preg_replace('#([a-z]*)[\x00-\x20]*=*([\'\"]*)[\x00-\x20]*-moz-binding[\x00-\x20]*:#iUu', '$1=$2nomozbinding...', $this->_cleaned); + $this->_cleaned = preg_replace('#([a-z]*)[\x00-\x20]*=([\'\"]*)[\x00-\x20]*data[\x00-\x20]*:#Uu', '$1=$2nodata...', $this->_cleaned); + $this->_cleaned = preg_replace('#(<[^>]+)style[\x00-\x20]*=[\x00-\x20]*([\`\'\"]*).*expression[\x00-\x20]*\([^>]*>#iU', "$1>", $this->_cleaned); + $this->_cleaned = preg_replace('#(<[^>]+)style[\x00-\x20]*=[\x00-\x20]*([\`\'\"]*).*behaviour[\x00-\x20]*\([^>]*>#iU', "$1>", $this->_cleaned); + $this->_cleaned = preg_replace('#(<[^>]+)style[\x00-\x20]*=[\x00-\x20]*([\`\'\"]*).*s[\x00-\x20]*c[\x00-\x20]*r[\x00-\x20]*i[\x00-\x20]*p[\x00-\x20]*t[\x00-\x20]*:*[^>]*>#iUu', "$1>", $this->_cleaned); + $this->_cleaned = preg_replace('#]*>#i', "", $this->_cleaned); + do { + $oldstring = $this->_cleaned; + $this->_cleaned = preg_replace('#]*>#i', "", $this->_cleaned); + } while ($oldstring !== $this->_cleaned); + $this->_cleaned = str_replace(["&", "<", ">"], ["&amp;", "&lt;", "&gt;"], $this->_cleaned); + } + + /** + * Gets the currently-used model of the rendering context. + * + * @return string + */ + public function model() + { + if ($this->_association) { + return $this->_association; + } + return $this->_modelScope; + } + + /** + * Adds the given class to the element options + * + * @param array $options Array options/attributes to add a class to + * @param string $class The class name being added. + * @param string $key the key to use for class. + * @return array Array of options with $key set. + */ + public function addClass($options = [], $class = null, $key = 'class') + { + if (isset($options[$key]) && trim($options[$key])) { + $options[$key] .= ' ' . $class; + } else { + $options[$key] = $class; + } + return $options; + } + + /** + * Returns a string generated by a helper method + * + * This method can be overridden in subclasses to do generalized output post-processing + * + * @param string $str String to be output. + * @return string + * @deprecated 3.0.0 This method will be removed in future versions. + */ + public function output($str) + { + return $str; + } + + /** + * Before render callback. beforeRender is called before the view file is rendered. + * + * Overridden in subclasses. + * + * @param string $viewFile The view file that is going to be rendered + * @return void + */ + public function beforeRender($viewFile) + { + } + + /** + * After render callback. afterRender is called after the view file is rendered + * but before the layout has been rendered. + * + * Overridden in subclasses. + * + * @param string $viewFile The view file that was rendered. + * @return void + */ + public function afterRender($viewFile) + { + } + + /** + * Before layout callback. beforeLayout is called before the layout is rendered. + * + * Overridden in subclasses. + * + * @param string $layoutFile The layout about to be rendered. + * @return void + */ + public function beforeLayout($layoutFile) + { + } + + /** + * After layout callback. afterLayout is called after the layout has rendered. + * + * Overridden in subclasses. + * + * @param string $layoutFile The layout file that was rendered. + * @return void + */ + public function afterLayout($layoutFile) + { + } + + /** + * Before render file callback. + * Called before any view fragment is rendered. + * + * Overridden in subclasses. + * + * @param string $viewFile The file about to be rendered. + * @return void + */ + public function beforeRenderFile($viewFile) + { + } + + /** + * After render file callback. + * Called after any view fragment is rendered. + * + * Overridden in subclasses. + * + * @param string $viewFile The file just be rendered. + * @param string $content The content that was rendered. + * @return void + */ + public function afterRenderFile($viewFile, $content) + { + } + + /** + * Returns a space-delimited string with items of the $options array. If a key + * of $options array happens to be one of those listed in `Helper::$_minimizedAttributes` + * + * And its value is one of: + * + * - '1' (string) + * - 1 (integer) + * - true (boolean) + * - 'true' (string) + * + * Then the value will be reset to be identical with key's name. + * If the value is not one of these 3, the parameter is not output. + * + * 'escape' is a special option in that it controls the conversion of + * attributes to their html-entity encoded equivalents. Set to false to disable html-encoding. + * + * If value for any option key is set to `null` or `false`, that option will be excluded from output. + * + * @param array $options Array of options. + * @param array $exclude Array of options to be excluded, the options here will not be part of the return. + * @param string $insertBefore String to be inserted before options. + * @param string $insertAfter String to be inserted after options. + * @return string Composed attributes. + * @deprecated 3.0.0 This method will be moved to HtmlHelper in 3.0 + */ + protected function _parseAttributes($options, $exclude = null, $insertBefore = ' ', $insertAfter = null) + { + if (!is_string($options)) { + $options = (array)$options + ['escape' => true]; + + if (!is_array($exclude)) { + $exclude = []; + } + + $exclude = ['escape' => true] + array_flip($exclude); + $escape = $options['escape']; + $attributes = []; + + foreach ($options as $key => $value) { + if (!isset($exclude[$key]) && $value !== false && $value !== null) { + $attributes[] = $this->_formatAttribute($key, $value, $escape); + } + } + $out = implode(' ', $attributes); + } else { + $out = $options; + } + return $out ? $insertBefore . $out . $insertAfter : ''; + } + + /** + * Formats an individual attribute, and returns the string value of the composed attribute. + * Works with minimized attributes that have the same value as their name such as 'disabled' and 'checked' + * + * @param string $key The name of the attribute to create + * @param string $value The value of the attribute to create. + * @param bool $escape Define if the value must be escaped + * @return string The composed attribute. + * @deprecated 3.0.0 This method will be moved to HtmlHelper in 3.0 + */ + protected function _formatAttribute($key, $value, $escape = true) + { + if (is_array($value)) { + $value = implode(' ', $value); + } + if (is_numeric($key)) { + return sprintf($this->_minimizedAttributeFormat, $value, $value); + } + $truthy = [1, '1', true, 'true', $key]; + $isMinimized = in_array($key, $this->_minimizedAttributes); + if ($isMinimized && in_array($value, $truthy, true)) { + return sprintf($this->_minimizedAttributeFormat, $key, $key); + } + if ($isMinimized) { + return ''; + } + return sprintf($this->_attributeFormat, $key, ($escape ? h($value) : $value)); + } + + /** + * Returns a string to be used as onclick handler for confirm dialogs. + * + * @param string $message Message to be displayed + * @param string $okCode Code to be executed after user chose 'OK' + * @param string $cancelCode Code to be executed after user chose 'Cancel', also executed when okCode doesn't return + * @param array $options Array of options + * @return string onclick JS code + */ + protected function _confirm($message, $okCode, $cancelCode = '', $options = []) + { + $message = json_encode($message); + $confirm = "if (confirm({$message})) { {$okCode} } {$cancelCode}"; + if (isset($options['escape']) && $options['escape'] === false) { + $confirm = h($confirm); + } + return $confirm; + } + + /** + * Sets the defaults for an input tag. Will set the + * name, value, and id attributes for an array of html attributes. + * + * @param string $field The field name to initialize. + * @param array $options Array of options to use while initializing an input field. + * @return array Array options for the form input. + */ + protected function _initInputField($field, $options = []) + { + if ($field !== null) { + $this->setEntity($field); + } + $options = (array)$options; + $options = $this->_name($options); + $options = $this->value($options); + $options = $this->domId($options); + return $options; + } + + /** + * Sets this helper's model and field properties to the dot-separated value-pair in $entity. + * + * @param string $entity A field name, like "ModelName.fieldName" or "ModelName.ID.fieldName" + * @param bool $setScope Sets the view scope to the model specified in $tagValue + * @return void + */ + public function setEntity($entity, $setScope = false) + { + if ($entity === null) { + $this->_modelScope = false; + } + if ($setScope === true) { + $this->_modelScope = $entity; + } + $parts = array_values(Hash::filter(explode('.', $entity))); + if (empty($parts)) { + return; + } + $count = count($parts); + $lastPart = isset($parts[$count - 1]) ? $parts[$count - 1] : null; + + // Either 'body' or 'date.month' type inputs. + if (($count === 1 && $this->_modelScope && !$setScope) || + ( + $count === 2 && + in_array($lastPart, $this->_fieldSuffixes) && + $this->_modelScope && + $parts[0] !== $this->_modelScope + ) + ) { + $entity = $this->_modelScope . '.' . $entity; + } + + // 0.name, 0.created.month style inputs. Excludes inputs with the modelScope in them. + if ($count >= 2 && + is_numeric($parts[0]) && + !is_numeric($parts[1]) && + $this->_modelScope && + strpos($entity, $this->_modelScope) === false + ) { + $entity = $this->_modelScope . '.' . $entity; + } + + $this->_association = null; + + $isHabtm = ( + isset($this->fieldset[$this->_modelScope]['fields'][$parts[0]]['type']) && + $this->fieldset[$this->_modelScope]['fields'][$parts[0]]['type'] === 'multiple' + ); + + // habtm models are special + if ($count === 1 && $isHabtm) { + $this->_association = $parts[0]; + $entity = $parts[0] . '.' . $parts[0]; + } else { + // check for associated model. + $reversed = array_reverse($parts); + foreach ($reversed as $i => $part) { + if ($i > 0 && preg_match('/^[A-Z]/', $part)) { + $this->_association = $part; + break; + } + } + } + $this->_entityPath = $entity; + } + + /** + * Gets the input field name for the current tag. Creates input name attributes + * using CakePHP's data[Model][field] formatting. + * + * @param array|string $options If an array, should be an array of attributes that $key needs to be added to. + * If a string or null, will be used as the View entity. + * @param string $field Field name. + * @param string $key The name of the attribute to be set, defaults to 'name' + * @return mixed If an array was given for $options, an array with $key set will be returned. + * If a string was supplied a string will be returned. + */ + protected function _name($options = [], $field = null, $key = 'name') + { + if ($options === null) { + $options = []; + } else if (is_string($options)) { + $field = $options; + $options = 0; + } + + if (!empty($field)) { + $this->setEntity($field); + } + + if (is_array($options) && array_key_exists($key, $options)) { + return $options; + } + + switch ($field) { + case '_method': + $name = $field; + break; + default: + $name = 'data[' . implode('][', $this->entity()) . ']'; + } + + if (is_array($options)) { + $options[$key] = $name; + return $options; + } + return $name; + } + + /** + * Returns the entity reference of the current context as an array of identity parts + * + * @return array An array containing the identity elements of an entity + */ + public function entity() + { + return explode('.', $this->_entityPath); + } + + /** + * Gets the data for the current tag + * + * @param array|string $options If an array, should be an array of attributes that $key needs to be added to. + * If a string or null, will be used as the View entity. + * @param string $field Field name. + * @param string $key The name of the attribute to be set, defaults to 'value' + * @return mixed If an array was given for $options, an array with $key set will be returned. + * If a string was supplied a string will be returned. + */ + public function value($options = [], $field = null, $key = 'value') + { + if ($options === null) { + $options = []; + } else if (is_string($options)) { + $field = $options; + $options = 0; + } + + if (is_array($options) && isset($options[$key])) { + return $options; + } + + if (!empty($field)) { + $this->setEntity($field); + } + $result = null; + $data = $this->request->data; + + $entity = $this->entity(); + if (!empty($data) && is_array($data) && !empty($entity)) { + $result = Hash::get($data, implode('.', $entity)); + } + + $habtmKey = $this->field(); + if (empty($result) && isset($data[$habtmKey][$habtmKey]) && is_array($data[$habtmKey])) { + $result = $data[$habtmKey][$habtmKey]; + } else if (empty($result) && isset($data[$habtmKey]) && is_array($data[$habtmKey])) { + if (ClassRegistry::isKeySet($habtmKey)) { + $model = ClassRegistry::getObject($habtmKey); + $result = $this->_selectedArray($data[$habtmKey], $model->primaryKey); + } + } + + if (is_array($options)) { + if ($result === null && isset($options['default'])) { + $result = $options['default']; + } + unset($options['default']); + } + + if (is_array($options)) { + $options[$key] = $result; + return $options; + } + return $result; + } + + /** + * Gets the currently-used model field of the rendering context. + * Strips off field suffixes such as year, month, day, hour, min, meridian + * when the current entity is longer than 2 elements. + * + * @return string + */ + public function field() + { + $entity = $this->entity(); + $count = count($entity); + $last = $entity[$count - 1]; + if ($count > 2 && in_array($last, $this->_fieldSuffixes)) { + $last = isset($entity[$count - 2]) ? $entity[$count - 2] : null; + } + return $last; + } + + /** + * Transforms a recordset from a hasAndBelongsToMany association to a list of selected + * options for a multiple select element + * + * @param string|array $data Data array or model name. + * @param string $key Field name. + * @return array + */ + protected function _selectedArray($data, $key = 'id') + { + if (!is_array($data)) { + $model = $data; + if (!empty($this->request->data[$model][$model])) { + return $this->request->data[$model][$model]; + } + if (!empty($this->request->data[$model])) { + $data = $this->request->data[$model]; + } + } + $array = []; + if (!empty($data)) { + foreach ($data as $row) { + if (isset($row[$key])) { + $array[$row[$key]] = $row[$key]; + } + } + } + return empty($array) ? null : $array; + } + + /** + * Generates a DOM ID for the selected element, if one is not set. + * Uses the current View::entity() settings to generate a CamelCased id attribute. + * + * @param array|string $options Either an array of html attributes to add $id into, or a string + * with a view entity path to get a domId for. + * @param string $id The name of the 'id' attribute. + * @return mixed If $options was an array, an array will be returned with $id set. If a string + * was supplied, a string will be returned. + */ + public function domId($options = null, $id = 'id') + { + if (is_array($options) && array_key_exists($id, $options) && $options[$id] === null) { + unset($options[$id]); + return $options; + } else if (!is_array($options) && $options !== null) { + $this->setEntity($options); + return $this->domId(); + } + + $entity = $this->entity(); + $model = array_shift($entity); + $dom = $model . implode('', array_map(['Inflector', 'camelize'], $entity)); + + if (is_array($options) && !array_key_exists($id, $options)) { + $options[$id] = $dom; + } else if ($options === null) { + return $dom; + } + return $options; + } } diff --git a/lib/Cake/View/Helper/CacheHelper.php b/lib/Cake/View/Helper/CacheHelper.php index 618c2f82..57e7f03d 100755 --- a/lib/Cake/View/Helper/CacheHelper.php +++ b/lib/Cake/View/Helper/CacheHelper.php @@ -27,290 +27,276 @@ * @deprecated This class will be removed in 3.0. You should use a separate response cache * like Varnish instead. */ -class CacheHelper extends AppHelper { - -/** - * Array of strings replaced in cached views. - * The strings are found between `` in views - * - * @var array - */ - protected $_replace = array(); - -/** - * Array of string that are replace with there var replace above. - * The strings are any content inside `` and includes the tags in views - * - * @var array - */ - protected $_match = array(); - -/** - * Counter used for counting nocache section tags. - * - * @var int - */ - protected $_counter = 0; - -/** - * Is CacheHelper enabled? should files + output be parsed. - * - * @return bool - */ - protected function _enabled() { - return $this->_View->cacheAction && (Configure::read('Cache.check') === true); - } - -/** - * Parses the view file and stores content for cache file building. - * - * @param string $viewFile View file name. - * @param string $output The output for the file. - * @return string Updated content. - */ - public function afterRenderFile($viewFile, $output) { - if ($this->_enabled()) { - return $this->_parseContent($viewFile, $output); - } - } - -/** - * Parses the layout file and stores content for cache file building. - * - * @param string $layoutFile Layout file name. - * @return void - */ - public function afterLayout($layoutFile) { - if ($this->_enabled()) { - $this->_View->output = $this->cache($layoutFile, $this->_View->output); - } - $this->_View->output = preg_replace('//', '', $this->_View->output); - } - -/** - * Parse a file + output. Matches nocache tags between the current output and the current file - * stores a reference of the file, so the generated can be swapped back with the file contents when - * writing the cache file. - * - * @param string $file The filename to process. - * @param string $out The output for the file. - * @return string Updated content. - */ - protected function _parseContent($file, $out) { - $out = preg_replace_callback('//', array($this, '_replaceSection'), $out); - $this->_parseFile($file, $out); - return $out; - } - -/** - * Main method used to cache a view - * - * @param string $file File to cache - * @param string $out output to cache - * @return string view output - * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/cache.html - * @throws Exception If debug mode is enabled and writing to cache file fails. - */ - public function cache($file, $out) { - $cacheTime = 0; - $useCallbacks = false; - $cacheAction = $this->_View->cacheAction; - - if (is_array($cacheAction)) { - $keys = array_keys($cacheAction); - $index = null; - - foreach ($keys as $action) { - if ($action === $this->request->params['action']) { - $index = $action; - break; - } - } - - if (!isset($index) && $this->request->params['action'] === 'index') { - $index = 'index'; - } - - $options = $cacheAction; - if (isset($cacheAction[$index])) { - if (is_array($cacheAction[$index])) { - $options = $cacheAction[$index] + array('duration' => 0, 'callbacks' => false); - } else { - $cacheTime = $cacheAction[$index]; - } - } - if (isset($options['duration'])) { - $cacheTime = $options['duration']; - } - if (isset($options['callbacks'])) { - $useCallbacks = $options['callbacks']; - } - } else { - $cacheTime = $cacheAction; - } - - if ($cacheTime && $cacheTime > 0) { - $cached = $this->_parseOutput($out); - try { - $this->_writeFile($cached, $cacheTime, $useCallbacks); - } catch (Exception $e) { - if (Configure::read('debug')) { - throw $e; - } - - $message = __d( - 'cake_dev', - 'Unable to write view cache file: "%s" for "%s"', - $e->getMessage(), - $this->request->here - ); - $this->log($message, 'error'); - } - $out = $this->_stripTags($out); - } - return $out; - } - -/** - * Parse file searching for no cache tags - * - * @param string $file The filename that needs to be parsed. - * @param string $cache The cached content - * @return void - */ - protected function _parseFile($file, $cache) { - if (is_file($file)) { - $file = file_get_contents($file); - } elseif ($file = fileExistsInPath($file)) { - $file = file_get_contents($file); - } - preg_match_all('/((?<=)[\\s\\S]*?(?=))/i', $cache, $outputResult, PREG_PATTERN_ORDER); - preg_match_all('/(?<=)([\\s\\S]*?)(?=)/i', $file, $fileResult, PREG_PATTERN_ORDER); - $fileResult = $fileResult[0]; - $outputResult = $outputResult[0]; - - if (!empty($this->_replace)) { - foreach ($outputResult as $i => $element) { - $index = array_search($element, $this->_match); - if ($index !== false) { - unset($outputResult[$i]); - } - } - $outputResult = array_values($outputResult); - } - - if (!empty($fileResult)) { - $i = 0; - foreach ($fileResult as $cacheBlock) { - if (isset($outputResult[$i])) { - $this->_replace[] = $cacheBlock; - $this->_match[] = $outputResult[$i]; - } - $i++; - } - } - } - -/** - * Munges the output from a view with cache tags, and numbers the sections. - * This helps solve issues with empty/duplicate content. - * - * @return string The content with cake:nocache tags replaced. - */ - protected function _replaceSection() { - $this->_counter += 1; - return sprintf('', $this->_counter); - } - -/** - * Strip cake:nocache tags from a string. Since View::render() - * only removes un-numbered nocache tags, remove all the numbered ones. - * This is the complement to _replaceSection. - * - * @param string $content String to remove tags from. - * @return string String with tags removed. - */ - protected function _stripTags($content) { - return preg_replace('##', '', $content); - } - -/** - * Parse the output and replace cache tags - * - * @param string $cache Output to replace content in. - * @return string with all replacements made to - */ - protected function _parseOutput($cache) { - $count = 0; - if (!empty($this->_match)) { - foreach ($this->_match as $found) { - $original = $cache; - $length = strlen($found); - $position = 0; - - for ($i = 1; $i <= 1; $i++) { - $position = strpos($cache, $found, $position); - - if ($position !== false) { - $cache = substr($original, 0, $position); - $cache .= $this->_replace[$count]; - $cache .= substr($original, $position + $length); - } else { - break; - } - } - $count++; - } - return $cache; - } - return $cache; - } - -/** - * Write a cached version of the file - * - * @param string $content view content to write to a cache file. - * @param string $timestamp Duration to set for cache file. - * @param bool|null $useCallbacks Whether to include statements in cached file which - * run callbacks, otherwise null. - * @return bool success of caching view. - */ - protected function _writeFile($content, $timestamp, $useCallbacks = false) { - $now = time(); - - if (is_numeric($timestamp)) { - $cacheTime = $now + $timestamp; - } else { - $cacheTime = strtotime($timestamp, $now); - } - $path = $this->request->here(); - if ($path === '/') { - $path = 'home'; - } - $prefix = Configure::read('Cache.viewPrefix'); - if ($prefix) { - $path = $prefix . '_' . $path; - } - $cache = strtolower(Inflector::slug($path)); - - if (empty($cache)) { - return null; - } - $cache = $cache . '.php'; - $file = '_View->plugin)) { - $file .= " +class CacheHelper extends AppHelper +{ + + /** + * Array of strings replaced in cached views. + * The strings are found between `` in views + * + * @var array + */ + protected $_replace = []; + + /** + * Array of string that are replace with there var replace above. + * The strings are any content inside `` and includes the tags in views + * + * @var array + */ + protected $_match = []; + + /** + * Counter used for counting nocache section tags. + * + * @var int + */ + protected $_counter = 0; + + /** + * Parses the view file and stores content for cache file building. + * + * @param string $viewFile View file name. + * @param string $output The output for the file. + * @return string Updated content. + */ + public function afterRenderFile($viewFile, $output) + { + if ($this->_enabled()) { + return $this->_parseContent($viewFile, $output); + } + } + + /** + * Is CacheHelper enabled? should files + output be parsed. + * + * @return bool + */ + protected function _enabled() + { + return $this->_View->cacheAction && (Configure::read('Cache.check') === true); + } + + /** + * Parse a file + output. Matches nocache tags between the current output and the current file + * stores a reference of the file, so the generated can be swapped back with the file contents when + * writing the cache file. + * + * @param string $file The filename to process. + * @param string $out The output for the file. + * @return string Updated content. + */ + protected function _parseContent($file, $out) + { + $out = preg_replace_callback('//', [$this, '_replaceSection'], $out); + $this->_parseFile($file, $out); + return $out; + } + + /** + * Parse file searching for no cache tags + * + * @param string $file The filename that needs to be parsed. + * @param string $cache The cached content + * @return void + */ + protected function _parseFile($file, $cache) + { + if (is_file($file)) { + $file = file_get_contents($file); + } else if ($file = fileExistsInPath($file)) { + $file = file_get_contents($file); + } + preg_match_all('/((?<=)[\\s\\S]*?(?=))/i', $cache, $outputResult, PREG_PATTERN_ORDER); + preg_match_all('/(?<=)([\\s\\S]*?)(?=)/i', $file, $fileResult, PREG_PATTERN_ORDER); + $fileResult = $fileResult[0]; + $outputResult = $outputResult[0]; + + if (!empty($this->_replace)) { + foreach ($outputResult as $i => $element) { + $index = array_search($element, $this->_match); + if ($index !== false) { + unset($outputResult[$i]); + } + } + $outputResult = array_values($outputResult); + } + + if (!empty($fileResult)) { + $i = 0; + foreach ($fileResult as $cacheBlock) { + if (isset($outputResult[$i])) { + $this->_replace[] = $cacheBlock; + $this->_match[] = $outputResult[$i]; + } + $i++; + } + } + } + + /** + * Parses the layout file and stores content for cache file building. + * + * @param string $layoutFile Layout file name. + * @return void + */ + public function afterLayout($layoutFile) + { + if ($this->_enabled()) { + $this->_View->output = $this->cache($layoutFile, $this->_View->output); + } + $this->_View->output = preg_replace('//', '', $this->_View->output); + } + + /** + * Main method used to cache a view + * + * @param string $file File to cache + * @param string $out output to cache + * @return string view output + * @throws Exception If debug mode is enabled and writing to cache file fails. + * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/cache.html + */ + public function cache($file, $out) + { + $cacheTime = 0; + $useCallbacks = false; + $cacheAction = $this->_View->cacheAction; + + if (is_array($cacheAction)) { + $keys = array_keys($cacheAction); + $index = null; + + foreach ($keys as $action) { + if ($action === $this->request->params['action']) { + $index = $action; + break; + } + } + + if (!isset($index) && $this->request->params['action'] === 'index') { + $index = 'index'; + } + + $options = $cacheAction; + if (isset($cacheAction[$index])) { + if (is_array($cacheAction[$index])) { + $options = $cacheAction[$index] + ['duration' => 0, 'callbacks' => false]; + } else { + $cacheTime = $cacheAction[$index]; + } + } + if (isset($options['duration'])) { + $cacheTime = $options['duration']; + } + if (isset($options['callbacks'])) { + $useCallbacks = $options['callbacks']; + } + } else { + $cacheTime = $cacheAction; + } + + if ($cacheTime && $cacheTime > 0) { + $cached = $this->_parseOutput($out); + try { + $this->_writeFile($cached, $cacheTime, $useCallbacks); + } catch (Exception $e) { + if (Configure::read('debug')) { + throw $e; + } + + $message = __d( + 'cake_dev', + 'Unable to write view cache file: "%s" for "%s"', + $e->getMessage(), + $this->request->here + ); + $this->log($message, 'error'); + } + $out = $this->_stripTags($out); + } + return $out; + } + + /** + * Parse the output and replace cache tags + * + * @param string $cache Output to replace content in. + * @return string with all replacements made to + */ + protected function _parseOutput($cache) + { + $count = 0; + if (!empty($this->_match)) { + foreach ($this->_match as $found) { + $original = $cache; + $length = strlen($found); + $position = 0; + + for ($i = 1; $i <= 1; $i++) { + $position = strpos($cache, $found, $position); + + if ($position !== false) { + $cache = substr($original, 0, $position); + $cache .= $this->_replace[$count]; + $cache .= substr($original, $position + $length); + } else { + break; + } + } + $count++; + } + return $cache; + } + return $cache; + } + + /** + * Write a cached version of the file + * + * @param string $content view content to write to a cache file. + * @param string $timestamp Duration to set for cache file. + * @param bool|null $useCallbacks Whether to include statements in cached file which + * run callbacks, otherwise null. + * @return bool success of caching view. + */ + protected function _writeFile($content, $timestamp, $useCallbacks = false) + { + $now = time(); + + if (is_numeric($timestamp)) { + $cacheTime = $now + $timestamp; + } else { + $cacheTime = strtotime($timestamp, $now); + } + $path = $this->request->here(); + if ($path === '/') { + $path = 'home'; + } + $prefix = Configure::read('Cache.viewPrefix'); + if ($prefix) { + $path = $prefix . '_' . $path; + } + $cache = strtolower(Inflector::slug($path)); + + if (empty($cache)) { + return null; + } + $cache = $cache . '.php'; + $file = '_View->plugin)) { + $file .= " App::uses('{$this->_View->name}Controller', 'Controller'); "; - } else { - $file .= " + } else { + $file .= " App::uses('{$this->_View->plugin}AppController', '{$this->_View->plugin}.Controller'); App::uses('{$this->_View->name}Controller', '{$this->_View->plugin}.Controller'); "; - } + } - $file .= ' + $file .= ' $request = unserialize(base64_decode(\'' . base64_encode(serialize($this->request)) . '\')); $response->type(\'' . $this->_View->response->type() . '\'); $controller = new ' . $this->_View->name . 'Controller($request, $response); @@ -322,20 +308,45 @@ protected function _writeFile($content, $timestamp, $useCallbacks = false) { Router::setRequestInfo($controller->request); $this->request = $request;'; - if ($useCallbacks) { - $file .= ' + if ($useCallbacks) { + $file .= ' $controller->constructClasses(); $controller->startupProcess();'; - } + } - $file .= ' + $file .= ' $this->viewVars = $controller->viewVars; $this->loadHelpers(); extract($this->viewVars, EXTR_SKIP); ?>'; - $content = preg_replace("/(<\\?xml)/", "", $content); - $file .= $content; - return cache('views' . DS . $cache, $file, $timestamp); - } + $content = preg_replace("/(<\\?xml)/", "", $content); + $file .= $content; + return cache('views' . DS . $cache, $file, $timestamp); + } + + /** + * Strip cake:nocache tags from a string. Since View::render() + * only removes un-numbered nocache tags, remove all the numbered ones. + * This is the complement to _replaceSection. + * + * @param string $content String to remove tags from. + * @return string String with tags removed. + */ + protected function _stripTags($content) + { + return preg_replace('##', '', $content); + } + + /** + * Munges the output from a view with cache tags, and numbers the sections. + * This helps solve issues with empty/duplicate content. + * + * @return string The content with cake:nocache tags replaced. + */ + protected function _replaceSection() + { + $this->_counter += 1; + return sprintf('', $this->_counter); + } } diff --git a/lib/Cake/View/Helper/FlashHelper.php b/lib/Cake/View/Helper/FlashHelper.php index 9dd4af7d..9c299de6 100644 --- a/lib/Cake/View/Helper/FlashHelper.php +++ b/lib/Cake/View/Helper/FlashHelper.php @@ -27,73 +27,75 @@ * * @package Cake.View.Helper */ -class FlashHelper extends AppHelper { +class FlashHelper extends AppHelper +{ -/** - * Used to render the message set in FlashComponent::set() - * - * In your view: $this->Flash->render('somekey'); - * Will default to flash if no param is passed - * - * You can pass additional information into the flash message generation. This allows you - * to consolidate all the parameters for a given type of flash message into the view. - * - * ``` - * echo $this->Flash->render('flash', array('params' => array('name' => $user['User']['name']))); - * ``` - * - * This would pass the current user's name into the flash message, so you could create personalized - * messages without the controller needing access to that data. - * - * Lastly you can choose the element that is used for rendering the flash message. Using - * custom elements allows you to fully customize how flash messages are generated. - * - * ``` - * echo $this->Flash->render('flash', array('element' => 'my_custom_element')); - * ``` - * - * If you want to use an element from a plugin for rendering your flash message - * you can use the dot notation for the plugin's element name: - * - * ``` - * echo $this->Flash->render('flash', array( - * 'element' => 'MyPlugin.my_custom_element', - * )); - * ``` - * - * @param string $key The [Message.]key you are rendering in the view. - * @param array $options Additional options to use for the creation of this flash message. - * Supports the 'params', and 'element' keys that are used in the helper. - * @return string|null Rendered flash message or null if flash key does not exist - * in session. - * @throws UnexpectedValueException If value for flash settings key is not an array. - */ - public function render($key = 'flash', $options = array()) { - if (!CakeSession::check("Message.$key")) { - return null; - } + /** + * Used to render the message set in FlashComponent::set() + * + * In your view: $this->Flash->render('somekey'); + * Will default to flash if no param is passed + * + * You can pass additional information into the flash message generation. This allows you + * to consolidate all the parameters for a given type of flash message into the view. + * + * ``` + * echo $this->Flash->render('flash', array('params' => array('name' => $user['User']['name']))); + * ``` + * + * This would pass the current user's name into the flash message, so you could create personalized + * messages without the controller needing access to that data. + * + * Lastly you can choose the element that is used for rendering the flash message. Using + * custom elements allows you to fully customize how flash messages are generated. + * + * ``` + * echo $this->Flash->render('flash', array('element' => 'my_custom_element')); + * ``` + * + * If you want to use an element from a plugin for rendering your flash message + * you can use the dot notation for the plugin's element name: + * + * ``` + * echo $this->Flash->render('flash', array( + * 'element' => 'MyPlugin.my_custom_element', + * )); + * ``` + * + * @param string $key The [Message.]key you are rendering in the view. + * @param array $options Additional options to use for the creation of this flash message. + * Supports the 'params', and 'element' keys that are used in the helper. + * @return string|null Rendered flash message or null if flash key does not exist + * in session. + * @throws UnexpectedValueException If value for flash settings key is not an array. + */ + public function render($key = 'flash', $options = []) + { + if (!CakeSession::check("Message.$key")) { + return null; + } - $flash = CakeSession::read("Message.$key"); + $flash = CakeSession::read("Message.$key"); - if (!is_array($flash)) { - throw new UnexpectedValueException(sprintf( - 'Value for flash setting key "%s" must be an array.', - $key - )); - } + if (!is_array($flash)) { + throw new UnexpectedValueException(sprintf( + 'Value for flash setting key "%s" must be an array.', + $key + )); + } - CakeSession::delete("Message.$key"); + CakeSession::delete("Message.$key"); - $out = ''; - foreach ($flash as $message) { - $message['key'] = $key; - $message = $options + $message; - if ($message['element'] === 'default') { - $message['element'] = 'Flash/default'; - } - $out .= $this->_View->element($message['element'], $message); - } + $out = ''; + foreach ($flash as $message) { + $message['key'] = $key; + $message = $options + $message; + if ($message['element'] === 'default') { + $message['element'] = 'Flash/default'; + } + $out .= $this->_View->element($message['element'], $message); + } - return $out; - } + return $out; + } } diff --git a/lib/Cake/View/Helper/FormHelper.php b/lib/Cake/View/Helper/FormHelper.php index f758b224..9bd1726b 100755 --- a/lib/Cake/View/Helper/FormHelper.php +++ b/lib/Cake/View/Helper/FormHelper.php @@ -28,61 +28,63 @@ * @property HtmlHelper $Html * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/form.html */ -class FormHelper extends AppHelper { +class FormHelper extends AppHelper +{ /** - * Other helpers used by FormHelper + * Constant used internally to skip the securing process, + * and neither add the field to the hash or to the unlocked fields. * - * @var array + * @var string */ - public $helpers = array('Html'); - + const SECURE_SKIP = 'skip'; /** - * Options used by DateTime fields + * Other helpers used by FormHelper * * @var array */ - protected $_options = array( - 'day' => array(), 'minute' => array(), 'hour' => array(), - 'month' => array(), 'year' => array(), 'meridian' => array() - ); - + public $helpers = ['Html']; /** * List of fields created, used with secure forms. * * @var array */ - public $fields = array(); - - /** - * Constant used internally to skip the securing process, - * and neither add the field to the hash or to the unlocked fields. - * - * @var string - */ - const SECURE_SKIP = 'skip'; - + public $fields = []; /** * Defines the type of form being created. Set by FormHelper::create(). * * @var string */ public $requestType = null; - /** * The default model being used for the current form. * * @var string */ public $defaultModel = null; - + /** + * Holds all the validation errors for models loaded and inspected + * it can also be set manually to be able to display custom error messages + * in the any of the input fields generated by this helper + * + * @var array + */ + public $validationErrors = []; + /** + * Options used by DateTime fields + * + * @var array + */ + protected $_options = [ + 'day' => [], 'minute' => [], 'hour' => [], + 'month' => [], 'year' => [], 'meridian' => [] + ]; /** * Persistent default options used by input(). Set by FormHelper::create(). * * @var array */ - protected $_inputDefaults = array(); - + protected $_inputDefaults = []; /** * An array of field names that have been excluded from * the Token hash used by SecurityComponent's validatePost method @@ -91,31 +93,20 @@ class FormHelper extends AppHelper { * @see SecurityComponent::validatePost() * @var array */ - protected $_unlockedFields = array(); - + protected $_unlockedFields = []; /** * Holds the model references already loaded by this helper * product of trying to inspect them out of field names * * @var array */ - protected $_models = array(); - - /** - * Holds all the validation errors for models loaded and inspected - * it can also be set manually to be able to display custom error messages - * in the any of the input fields generated by this helper - * - * @var array - */ - public $validationErrors = array(); - + protected $_models = []; /** * Holds already used DOM ID suffixes to avoid collisions with multiple form field elements. * * @var array */ - protected $_domIdSuffixes = array(); + protected $_domIdSuffixes = []; /** * The action attribute value of the last created form. @@ -131,53 +122,56 @@ class FormHelper extends AppHelper { * @param View $View The View this helper is being attached to. * @param array $settings Configuration settings for the helper. */ - public function __construct(View $View, $settings = array()) { + public function __construct(View $View, $settings = []) + { parent::__construct($View, $settings); $this->validationErrors =& $View->validationErrors; } /** - * Guess the location for a model based on its name and tries to create a new instance - * or get an already created instance of the model + * Returns true if there is an error for the given field, otherwise false * - * @param string $model Model name. - * @return Model|null Model instance + * @param string $field This should be "Modelname.fieldname" + * @return bool If there are errors this method returns true, else false. + * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/form.html#FormHelper::isFieldError */ - protected function _getModel($model) { - $object = null; - if (!$model || $model === 'Model') { - return $object; - } + public function isFieldError($field) + { + $this->setEntity($field); + return (bool)$this->tagIsInvalid(); + } - if (array_key_exists($model, $this->_models)) { - return $this->_models[$model]; - } + /** + * Returns false if given form field described by the current entity has no errors. + * Otherwise it returns the validation message + * + * @return mixed Either false when there are no errors, or an array of error + * strings. An error string could be ''. + * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/form.html#FormHelper::tagIsInvalid + */ + public function tagIsInvalid() + { + $entity = $this->entity(); + $model = array_shift($entity); - if (ClassRegistry::isKeySet($model)) { - $object = ClassRegistry::getObject($model); - } elseif (isset($this->request->params['models'][$model])) { - $plugin = $this->request->params['models'][$model]['plugin']; - $plugin .= ($plugin) ? '.' : null; - $object = ClassRegistry::init(array( - 'class' => $plugin . $this->request->params['models'][$model]['className'], - 'alias' => $model - )); - } elseif (ClassRegistry::isKeySet($this->defaultModel)) { - $defaultObject = ClassRegistry::getObject($this->defaultModel); - if ($defaultObject && in_array($model, array_keys($defaultObject->getAssociated()), true) && isset($defaultObject->{$model})) { - $object = $defaultObject->{$model}; - } - } else { - $object = ClassRegistry::init($model, true); + // 0.Model.field. Fudge entity path + if (empty($model) || is_numeric($model)) { + array_splice($entity, 1, 0, $model); + $model = array_shift($entity); } - $this->_models[$model] = $object; - if (!$object) { - return null; + $errors = []; + if (!empty($entity) && isset($this->validationErrors[$model])) { + $errors = $this->validationErrors[$model]; } - - $this->fieldset[$model] = array('fields' => null, 'key' => $object->primaryKey, 'validates' => null); - return $object; + if (!empty($entity) && empty($errors)) { + $errors = $this->_introspectModel($model, 'errors'); + } + if (empty($errors)) { + return false; + } + $errors = Hash::get($errors, implode('.', $entity)); + return $errors === null ? false : $errors; } /** @@ -200,7 +194,8 @@ protected function _getModel($model) { * @param string $field name of the model field to get information from * @return mixed information extracted for the special key and field in a model */ - protected function _introspectModel($model, $key, $field = null) { + protected function _introspectModel($model, $key, $field = null) + { $object = $this->_getModel($model); if (!$object) { return null; @@ -214,26 +209,26 @@ protected function _introspectModel($model, $key, $field = null) { if (!isset($this->fieldset[$model]['fields'])) { $this->fieldset[$model]['fields'] = $object->schema(); foreach ($object->hasAndBelongsToMany as $alias => $assocData) { - $this->fieldset[$object->alias]['fields'][$alias] = array('type' => 'multiple'); + $this->fieldset[$object->alias]['fields'][$alias] = ['type' => 'multiple']; } } if ($field === null || $field === false) { return $this->fieldset[$model]['fields']; - } elseif (isset($this->fieldset[$model]['fields'][$field])) { + } else if (isset($this->fieldset[$model]['fields'][$field])) { return $this->fieldset[$model]['fields'][$field]; } - return isset($object->hasAndBelongsToMany[$field]) ? array('type' => 'multiple') : null; + return isset($object->hasAndBelongsToMany[$field]) ? ['type' => 'multiple'] : null; } if ($key === 'errors' && !isset($this->validationErrors[$model])) { $this->validationErrors[$model] =& $object->validationErrors; return $this->validationErrors[$model]; - } elseif ($key === 'errors' && isset($this->validationErrors[$model])) { + } else if ($key === 'errors' && isset($this->validationErrors[$model])) { return $this->validationErrors[$model]; } if ($key === 'validates' && !isset($this->fieldset[$model]['validates'])) { - $validates = array(); + $validates = []; foreach (iterator_to_array($object->validator(), true) as $validateField => $validateProperties) { if ($this->_isRequiredField($validateProperties)) { $validates[$validateField] = true; @@ -251,13 +246,59 @@ protected function _introspectModel($model, $key, $field = null) { } } + /** + * Guess the location for a model based on its name and tries to create a new instance + * or get an already created instance of the model + * + * @param string $model Model name. + * @return Model|null Model instance + */ + protected function _getModel($model) + { + $object = null; + if (!$model || $model === 'Model') { + return $object; + } + + if (array_key_exists($model, $this->_models)) { + return $this->_models[$model]; + } + + if (ClassRegistry::isKeySet($model)) { + $object = ClassRegistry::getObject($model); + } else if (isset($this->request->params['models'][$model])) { + $plugin = $this->request->params['models'][$model]['plugin']; + $plugin .= ($plugin) ? '.' : null; + $object = ClassRegistry::init([ + 'class' => $plugin . $this->request->params['models'][$model]['className'], + 'alias' => $model + ]); + } else if (ClassRegistry::isKeySet($this->defaultModel)) { + $defaultObject = ClassRegistry::getObject($this->defaultModel); + if ($defaultObject && in_array($model, array_keys($defaultObject->getAssociated()), true) && isset($defaultObject->{$model})) { + $object = $defaultObject->{$model}; + } + } else { + $object = ClassRegistry::init($model, true); + } + + $this->_models[$model] = $object; + if (!$object) { + return null; + } + + $this->fieldset[$model] = ['fields' => null, 'key' => $object->primaryKey, 'validates' => null]; + return $object; + } + /** * Returns if a field is required to be filled based on validation properties from the validating object. * * @param CakeValidationSet $validationRules Validation rules set. * @return bool true if field is required to be filled, false otherwise */ - protected function _isRequiredField($validationRules) { + protected function _isRequiredField($validationRules) + { if (empty($validationRules) || count($validationRules) === 0) { return false; } @@ -275,549 +316,530 @@ protected function _isRequiredField($validationRules) { } /** - * Returns false if given form field described by the current entity has no errors. - * Otherwise it returns the validation message + * Generate a set of inputs for `$fields`. If $fields is null the fields of current model + * will be used. * - * @return mixed Either false when there are no errors, or an array of error - * strings. An error string could be ''. - * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/form.html#FormHelper::tagIsInvalid + * You can customize individual inputs through `$fields`. + * ``` + * $this->Form->inputs(array( + * 'name' => array('label' => 'custom label') + * )); + * ``` + * + * In addition to controller fields output, `$fields` can be used to control legend + * and fieldset rendering. + * `$this->Form->inputs('My legend');` Would generate an input set with a custom legend. + * Passing `fieldset` and `legend` key in `$fields` array has been deprecated since 2.3, + * for more fine grained control use the `fieldset` and `legend` keys in `$options` param. + * + * @param array $fields An array of fields to generate inputs for, or null. + * @param array $blacklist A simple array of fields to not create inputs for. + * @param array $options Options array. Valid keys are: + * - `fieldset` Set to false to disable the fieldset. If a string is supplied it will be used as + * the class name for the fieldset element. + * - `legend` Set to false to disable the legend for the generated input set. Or supply a string + * to customize the legend text. + * @return string Completed form inputs. + * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/form.html#FormHelper::inputs */ - public function tagIsInvalid() { - $entity = $this->entity(); - $model = array_shift($entity); + public function inputs($fields = null, $blacklist = null, $options = []) + { + $fieldset = $legend = true; + $modelFields = []; + $model = $this->model(); + if ($model) { + $modelFields = array_keys((array)$this->_introspectModel($model, 'fields')); + } + if (is_array($fields)) { + if (array_key_exists('legend', $fields) && !in_array('legend', $modelFields)) { + $legend = $fields['legend']; + unset($fields['legend']); + } - // 0.Model.field. Fudge entity path - if (empty($model) || is_numeric($model)) { - array_splice($entity, 1, 0, $model); - $model = array_shift($entity); + if (isset($fields['fieldset']) && !in_array('fieldset', $modelFields)) { + $fieldset = $fields['fieldset']; + unset($fields['fieldset']); + } + } else if ($fields !== null) { + $fieldset = $legend = $fields; + if (!is_bool($fieldset)) { + $fieldset = true; + } + $fields = []; } - $errors = array(); - if (!empty($entity) && isset($this->validationErrors[$model])) { - $errors = $this->validationErrors[$model]; + if (isset($options['legend'])) { + $legend = $options['legend']; + unset($options['legend']); } - if (!empty($entity) && empty($errors)) { - $errors = $this->_introspectModel($model, 'errors'); + + if (isset($options['fieldset'])) { + $fieldset = $options['fieldset']; + unset($options['fieldset']); } - if (empty($errors)) { - return false; + + if (empty($fields)) { + $fields = $modelFields; } - $errors = Hash::get($errors, implode('.', $entity)); - return $errors === null ? false : $errors; + + if ($legend === true) { + $actionName = __d('cake', 'New %s'); + $isEdit = ( + strpos($this->request->params['action'], 'update') !== false || + strpos($this->request->params['action'], 'edit') !== false + ); + if ($isEdit) { + $actionName = __d('cake', 'Edit %s'); + } + $modelName = Inflector::humanize(Inflector::underscore($model)); + $legend = sprintf($actionName, __($modelName)); + } + + $out = null; + foreach ($fields as $name => $options) { + if (is_numeric($name) && !is_array($options)) { + $name = $options; + $options = []; + } + $entity = explode('.', $name); + $blacklisted = ( + is_array($blacklist) && + (in_array($name, $blacklist) || in_array(end($entity), $blacklist)) + ); + if ($blacklisted) { + continue; + } + $out .= $this->input($name, $options); + } + + if (is_string($fieldset)) { + $fieldsetClass = ['class' => $fieldset]; + } else { + $fieldsetClass = ''; + } + + if ($fieldset) { + if ($legend) { + $out = $this->Html->useTag('legend', $legend) . $out; + } + $out = $this->Html->useTag('fieldset', $fieldsetClass, $out); + } + return $out; } /** - * Returns an HTML FORM element. + * Generates a form input element complete with label and wrapper div * - * ### Options: + * ### Options * - * - `type` Form method defaults to POST - * - `action` The controller action the form submits to, (optional). Deprecated since 2.8, use `url`. - * - `url` The URL the form submits to. Can be a string or a URL array. If you use 'url' - * you should leave 'action' undefined. - * - `default` Allows for the creation of AJAX forms. Set this to false to prevent the default event handler. - * Will create an onsubmit attribute if it doesn't not exist. If it does, default action suppression - * will be appended. - * - `onsubmit` Used in conjunction with 'default' to create AJAX forms. - * - `inputDefaults` set the default $options for FormHelper::input(). Any options that would - * be set when using FormHelper::input() can be set here. Options set with `inputDefaults` - * can be overridden when calling input() - * - `encoding` Set the accept-charset encoding for the form. Defaults to `Configure::read('App.encoding')` + * See each field type method for more information. Any options that are part of + * $attributes or $options for the different **type** methods can be included in `$options` for input().i + * Additionally, any unknown keys that are not in the list below, or part of the selected type's options + * will be treated as a regular html attribute for the generated input. * - * @param mixed|null $model The model name for which the form is being defined. Should - * include the plugin name for plugin models. e.g. `ContactManager.Contact`. - * If an array is passed and $options argument is empty, the array will be used as options. - * If `false` no model is used. - * @param array $options An array of html attributes and options. - * @return string A formatted opening FORM tag. - * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/form.html#options-for-create + * - `type` - Force the type of widget you want. e.g. `type => 'select'` + * - `label` - Either a string label, or an array of options for the label. See FormHelper::label(). + * - `div` - Either `false` to disable the div, or an array of options for the div. + * See HtmlHelper::div() for more options. + * - `options` - For widgets that take options e.g. radio, select. + * - `error` - Control the error message that is produced. Set to `false` to disable any kind of error reporting (field + * error and error messages). + * - `errorMessage` - Boolean to control rendering error messages (field error will still occur). + * - `empty` - String or boolean to enable empty select box options. + * - `before` - Content to place before the label + input. + * - `after` - Content to place after the label + input. + * - `between` - Content to place between the label + input. + * - `format` - Format template for element order. Any element that is not in the array, will not be in the output. + * - Default input format order: array('before', 'label', 'between', 'input', 'after', 'error') + * - Default checkbox format order: array('before', 'input', 'between', 'label', 'after', 'error') + * - Hidden input will not be formatted + * - Radio buttons cannot have the order of input and label elements controlled with these settings. + * + * @param string $fieldName This should be "Modelname.fieldname" + * @param array $options Each type of input takes different options. + * @return string Completed form widget. + * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/form.html#creating-form-elements */ - public function create($model = null, $options = array()) { - $created = $id = false; - $append = ''; + public function input($fieldName, $options = []) + { + $this->setEntity($fieldName); + $options = $this->_parseOptions($options); - if (is_array($model) && empty($options)) { - $options = $model; - $model = null; - } + $divOptions = $this->_divOptions($options); + unset($options['div']); - if (empty($model) && $model !== false && !empty($this->request->params['models'])) { - $model = key($this->request->params['models']); - } elseif (empty($model) && empty($this->request->params['models'])) { - $model = false; + if ($options['type'] === 'radio' && isset($options['options'])) { + $radioOptions = (array)$options['options']; + unset($options['options']); + } else { + $radioOptions = []; } - $this->defaultModel = $model; - $key = null; - if ($model !== false) { - list($plugin, $model) = pluginSplit($model, true); - $key = $this->_introspectModel($plugin . $model, 'key'); - $this->setEntity($model, true); + $label = $this->_getLabel($fieldName, $options); + if ($options['type'] !== 'radio') { + unset($options['label']); } - if ($model !== false && $key) { - $recordExists = ( - isset($this->request->data[$model]) && - !empty($this->request->data[$model][$key]) && - !is_array($this->request->data[$model][$key]) - ); + $error = $this->_extractOption('error', $options, null); + unset($options['error']); - if ($recordExists) { - $created = true; - $id = $this->request->data[$model][$key]; - } - } + $errorMessage = $this->_extractOption('errorMessage', $options, true); + unset($options['errorMessage']); - $options += array( - 'type' => ($created && empty($options['action'])) ? 'put' : 'post', - 'action' => null, - 'url' => null, - 'default' => true, - 'encoding' => strtolower(Configure::read('App.encoding')), - 'inputDefaults' => array() - ); - $this->inputDefaults($options['inputDefaults']); - unset($options['inputDefaults']); + $selected = $this->_extractOption('selected', $options, null); + unset($options['selected']); - if (isset($options['action'])) { - trigger_error('Using key `action` is deprecated, use `url` directly instead.', E_USER_DEPRECATED); + if ($options['type'] === 'datetime' || $options['type'] === 'date' || $options['type'] === 'time') { + $dateFormat = $this->_extractOption('dateFormat', $options, 'MDY'); + $timeFormat = $this->_extractOption('timeFormat', $options, 12); + unset($options['dateFormat'], $options['timeFormat']); + } else { + $dateFormat = 'MDY'; + $timeFormat = 12; } - if (is_array($options['url']) && isset($options['url']['action'])) { - $options['action'] = $options['url']['action']; - } + $type = $options['type']; + $out = ['before' => $options['before'], 'label' => $label, 'between' => $options['between'], 'after' => $options['after']]; + $format = $this->_getFormat($options); - if (!isset($options['id'])) { - $domId = isset($options['action']) ? $options['action'] : $this->request['action']; - $options['id'] = $this->domId($domId . 'Form'); - } + unset($options['type'], $options['before'], $options['between'], $options['after'], $options['format']); - if ($options['action'] === null && $options['url'] === null) { - $options['action'] = $this->request->here(false); - } elseif (empty($options['url']) || is_array($options['url'])) { - if (empty($options['url']['controller'])) { - if (!empty($model)) { - $options['url']['controller'] = Inflector::underscore(Inflector::pluralize($model)); - } elseif (!empty($this->request->params['controller'])) { - $options['url']['controller'] = Inflector::underscore($this->request->params['controller']); + $out['error'] = null; + if ($type !== 'hidden' && $error !== false) { + $errMsg = $this->error($fieldName, $error); + if ($errMsg) { + $divOptions = $this->addClass($divOptions, Hash::get($divOptions, 'errorClass', 'error')); + if ($errorMessage) { + $out['error'] = $errMsg; } } - if (empty($options['action'])) { - $options['action'] = $this->request->params['action']; - } - - $plugin = null; - if ($this->plugin) { - $plugin = Inflector::underscore($this->plugin); - } - $actionDefaults = array( - 'plugin' => $plugin, - 'controller' => $this->_View->viewPath, - 'action' => $options['action'], - ); - $options['action'] = array_merge($actionDefaults, (array)$options['url']); - if (!isset($options['action'][0]) && !empty($id)) { - $options['action'][0] = $id; - } - } elseif (is_string($options['url'])) { - $options['action'] = $options['url']; - } - - switch (strtolower($options['type'])) { - case 'get': - $htmlAttributes['method'] = 'get'; - break; - case 'file': - $htmlAttributes['enctype'] = 'multipart/form-data'; - $options['type'] = ($created) ? 'put' : 'post'; - case 'post': - case 'put': - case 'delete': - $append .= $this->hidden('_method', array( - 'name' => '_method', 'value' => strtoupper($options['type']), 'id' => null, - 'secure' => static::SECURE_SKIP - )); - default: - $htmlAttributes['method'] = 'post'; } - $this->requestType = strtolower($options['type']); - $action = null; - if ($options['action'] !== false && $options['url'] !== false) { - $action = $this->url($options['action']); + if ($type === 'radio' && isset($out['between'])) { + $options['between'] = $out['between']; + $out['between'] = null; } - unset($options['url']); - - $this->_lastAction($options['action']); - unset($options['type'], $options['action']); + $out['input'] = $this->_getInput(compact('type', 'fieldName', 'options', 'radioOptions', 'selected', 'dateFormat', 'timeFormat')); - if (!$options['default']) { - if (!isset($options['onsubmit'])) { - $options['onsubmit'] = ''; - } - $htmlAttributes['onsubmit'] = $options['onsubmit'] . 'event.returnValue = false; return false;'; + $output = ''; + foreach ($format as $element) { + $output .= $out[$element]; } - unset($options['default']); - if (!empty($options['encoding'])) { - $htmlAttributes['accept-charset'] = $options['encoding']; - unset($options['encoding']); + if (!empty($divOptions['tag'])) { + $tag = $divOptions['tag']; + unset($divOptions['tag'], $divOptions['errorClass']); + $output = $this->Html->tag($tag, $output, $divOptions); } + return $output; + } - $htmlAttributes = array_merge($options, $htmlAttributes); + /** + * Generates input options array + * + * @param array $options Options list. + * @return array Options + */ + protected function _parseOptions($options) + { + $options = array_merge( + ['before' => null, 'between' => null, 'after' => null, 'format' => null], + $this->_inputDefaults, + $options + ); - $this->fields = array(); - if ($this->requestType !== 'get') { - $append .= $this->_csrfField(); + if (!isset($options['type'])) { + $options = $this->_magicOptions($options); } - if (!empty($append)) { - $append = $this->Html->useTag('hiddenblock', $append); + if (in_array($options['type'], ['radio', 'select'])) { + $options = $this->_optionsOptions($options); } - if ($model !== false) { - $this->setEntity($model, true); - $this->_introspectModel($model, 'fields'); - } + $options = $this->_maxLength($options); - if ($action === null) { - return $this->Html->useTag('formwithoutaction', $htmlAttributes) . $append; + if (isset($options['rows']) || isset($options['cols'])) { + $options['type'] = 'textarea'; } - return $this->Html->useTag('form', $action, $htmlAttributes) . $append; - } - - /** - * Return a CSRF input if the _Token is present. - * Used to secure forms in conjunction with SecurityComponent - * - * @return string - */ - protected function _csrfField() { - if (empty($this->request->params['_Token'])) { - return ''; - } - if (!empty($this->request['_Token']['unlockedFields'])) { - foreach ((array)$this->request['_Token']['unlockedFields'] as $unlocked) { - $this->_unlockedFields[] = $unlocked; - } + if ($options['type'] === 'datetime' || $options['type'] === 'date' || $options['type'] === 'time' || $options['type'] === 'select') { + $options += ['empty' => false]; } - return $this->hidden('_Token.key', array( - 'value' => $this->request->params['_Token']['key'], 'id' => 'Token' . mt_rand(), - 'secure' => static::SECURE_SKIP, - 'autocomplete' => 'off', - )); + return $options; } /** - * Closes an HTML form, cleans up values set by FormHelper::create(), and writes hidden - * input fields where appropriate. - * - * If $options is set a form submit button will be created. Options can be either a string or an array. - * - * ``` - * array usage: - * - * array('label' => 'save'); value="save" - * array('label' => 'save', 'name' => 'Whatever'); value="save" name="Whatever" - * array('name' => 'Whatever'); value="Submit" name="Whatever" - * array('label' => 'save', 'name' => 'Whatever', 'div' => 'good')
value="save" name="Whatever" - * array('label' => 'save', 'name' => 'Whatever', 'div' => array('class' => 'good'));
value="save" name="Whatever" - * ``` - * - * If $secureAttributes is set, these html attributes will be merged into the hidden input tags generated for the - * Security Component. This is especially useful to set HTML5 attributes like 'form' + * Magically set option type and corresponding options * - * @param string|array $options as a string will use $options as the value of button, - * @param array $secureAttributes will be passed as html attributes into the hidden input elements generated for the - * Security Component. - * @return string a closing FORM tag optional submit button. - * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/form.html#closing-the-form + * @param array $options Options list. + * @return array */ - public function end($options = null, $secureAttributes = array()) { - $out = null; - $submit = null; + protected function _magicOptions($options) + { + $modelKey = $this->model(); + $fieldKey = $this->field(); + $options['type'] = 'text'; + if (isset($options['options'])) { + $options['type'] = 'select'; + } else if (in_array($fieldKey, ['psword', 'passwd', 'password'])) { + $options['type'] = 'password'; + } else if (in_array($fieldKey, ['tel', 'telephone', 'phone'])) { + $options['type'] = 'tel'; + } else if ($fieldKey === 'email') { + $options['type'] = 'email'; + } else if (isset($options['checked'])) { + $options['type'] = 'checkbox'; + } else if ($fieldDef = $this->_introspectModel($modelKey, 'fields', $fieldKey)) { + $type = $fieldDef['type']; + $primaryKey = $this->fieldset[$modelKey]['key']; + $map = [ + 'string' => 'text', + 'datetime' => 'datetime', + 'boolean' => 'checkbox', + 'timestamp' => 'datetime', + 'text' => 'textarea', + 'time' => 'time', + 'date' => 'date', + 'float' => 'number', + 'integer' => 'number', + 'smallinteger' => 'number', + 'tinyinteger' => 'number', + 'decimal' => 'number', + 'binary' => 'file' + ]; - if ($options !== null) { - $submitOptions = array(); - if (is_string($options)) { - $submit = $options; - } else { - if (isset($options['label'])) { - $submit = $options['label']; - unset($options['label']); + if (isset($this->map[$type])) { + $options['type'] = $this->map[$type]; + } else if (isset($map[$type])) { + $options['type'] = $map[$type]; + } + if ($fieldKey === $primaryKey) { + $options['type'] = 'hidden'; + } + if ($options['type'] === 'number' && + !isset($options['step']) + ) { + if ($type === 'decimal' && isset($fieldDef['length'])) { + $decimalPlaces = substr($fieldDef['length'], strpos($fieldDef['length'], ',') + 1); + $options['step'] = sprintf('%.' . $decimalPlaces . 'F', pow(10, -1 * $decimalPlaces)); + } else if ($type === 'float' || $type === 'decimal') { + $options['step'] = 'any'; } - $submitOptions = $options; } - $out .= $this->submit($submit, $submitOptions); } - if ($this->requestType !== 'get' && - isset($this->request['_Token']) && - !empty($this->request['_Token']) - ) { - $out .= $this->secure($this->fields, $secureAttributes); - $this->fields = array(); + + if (preg_match('/_id$/', $fieldKey) && $options['type'] !== 'hidden') { + $options['type'] = 'select'; } - $this->setEntity(null); - $out .= $this->Html->useTag('formend'); - $this->_unlockedFields = array(); - $this->_View->modelScope = false; - $this->requestType = null; - return $out; + if ($modelKey === $fieldKey) { + $options['type'] = 'select'; + if (!isset($options['multiple'])) { + $options['multiple'] = 'multiple'; + } + } + if (in_array($options['type'], ['text', 'number'])) { + $options = $this->_optionsOptions($options); + } + if ($options['type'] === 'select' && array_key_exists('step', $options)) { + unset($options['step']); + } + + return $options; } /** - * Generates a hidden field with a security hash based on the fields used in - * the form. - * - * If $secureAttributes is set, these html attributes will be merged into - * the hidden input tags generated for the Security Component. This is - * especially useful to set HTML5 attributes like 'form'. + * Generates list of options for multiple select * - * @param array|null $fields If set specifies the list of fields to use when - * generating the hash, else $this->fields is being used. - * @param array $secureAttributes will be passed as html attributes into the hidden - * input elements generated for the Security Component. - * @return string|null A hidden input field with a security hash, otherwise null. - * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/form.html#FormHelper::secure + * @param array $options Options list. + * @return array */ - public function secure($fields = array(), $secureAttributes = array()) { - if (!isset($this->request['_Token']) || empty($this->request['_Token'])) { - return null; - } - $debugSecurity = Configure::read('debug'); - if (isset($secureAttributes['debugSecurity'])) { - $debugSecurity = $debugSecurity && $secureAttributes['debugSecurity']; - unset($secureAttributes['debugSecurity']); - } - - $originalFields = $fields; - - $locked = array(); - $unlockedFields = $this->_unlockedFields; - - foreach ($fields as $key => $value) { - if (!is_int($key)) { - $locked[$key] = $value; - unset($fields[$key]); - } + protected function _optionsOptions($options) + { + if (isset($options['options'])) { + return $options; } - - sort($unlockedFields, SORT_STRING); - sort($fields, SORT_STRING); - ksort($locked, SORT_STRING); - $fields += $locked; - - $locked = implode('|', array_keys($locked)); - $unlocked = implode('|', $unlockedFields); - $hashParts = array( - $this->_lastAction, - serialize($fields), - $unlocked, - Configure::read('Security.salt') + $varName = Inflector::variable( + Inflector::pluralize(preg_replace('/_id$/', '', $this->field())) ); - $fields = Security::hash(implode('', $hashParts), 'sha1'); - - $tokenFields = array_merge($secureAttributes, array( - 'value' => urlencode($fields . ':' . $locked), - 'id' => 'TokenFields' . mt_rand(), - 'secure' => static::SECURE_SKIP, - 'autocomplete' => 'off', - )); - $out = $this->hidden('_Token.fields', $tokenFields); - $tokenUnlocked = array_merge($secureAttributes, array( - 'value' => urlencode($unlocked), - 'id' => 'TokenUnlocked' . mt_rand(), - 'secure' => static::SECURE_SKIP, - 'autocomplete' => 'off', - )); - $out .= $this->hidden('_Token.unlocked', $tokenUnlocked); - if ($debugSecurity) { - $tokenDebug = array_merge($secureAttributes, array( - 'value' => urlencode(json_encode(array( - $this->_lastAction, - $originalFields, - $this->_unlockedFields - ))), - 'id' => 'TokenDebug' . mt_rand(), - 'secure' => static::SECURE_SKIP, - )); - $out .= $this->hidden('_Token.debug', $tokenDebug); + $varOptions = $this->_View->get($varName); + if (!is_array($varOptions)) { + return $options; } - - return $this->Html->useTag('hiddenblock', $out); + if ($options['type'] !== 'radio') { + $options['type'] = 'select'; + } + $options['options'] = $varOptions; + return $options; } /** - * Add to or get the list of fields that are currently unlocked. - * Unlocked fields are not included in the field hash used by SecurityComponent - * unlocking a field once its been added to the list of secured fields will remove - * it from the list of fields. + * Calculates maxlength option * - * @param string $name The dot separated name for the field. - * @return mixed Either null, or the list of fields. - * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/form.html#FormHelper::unlockField + * @param array $options Options list. + * @return array */ - public function unlockField($name = null) { - if ($name === null) { - return $this->_unlockedFields; - } - if (!in_array($name, $this->_unlockedFields)) { - $this->_unlockedFields[] = $name; - } - $index = array_search($name, $this->fields); - if ($index !== false) { - unset($this->fields[$index]); + protected function _maxLength($options) + { + $fieldDef = $this->_introspectModel($this->model(), 'fields', $this->field()); + $autoLength = ( + !array_key_exists('maxlength', $options) && + isset($fieldDef['length']) && + is_scalar($fieldDef['length']) && + $fieldDef['length'] < 1000000 && + $fieldDef['type'] !== 'decimal' && + $fieldDef['type'] !== 'time' && + $fieldDef['type'] !== 'datetime' && + $options['type'] !== 'select' + ); + if ($autoLength && + in_array($options['type'], ['text', 'textarea', 'email', 'tel', 'url', 'search']) + ) { + $options['maxlength'] = (int)$fieldDef['length']; } - unset($this->fields[$name]); + return $options; } /** - * Determine which fields of a form should be used for hash. - * Populates $this->fields + * Generate div options for input * - * @param bool $lock Whether this field should be part of the validation - * or excluded as part of the unlockedFields. - * @param string|array $field Reference to field to be secured. Should be dot separated to indicate nesting. - * @param mixed $value Field value, if value should not be tampered with. - * @return void + * @param array $options Options list. + * @return array */ - protected function _secure($lock, $field = null, $value = null) { - if (!$field) { - $field = $this->entity(); - } elseif (is_string($field)) { - $field = explode('.', $field); + protected function _divOptions($options) + { + if ($options['type'] === 'hidden') { + return []; } - if (is_array($field)) { - $field = Hash::filter($field); + $div = $this->_extractOption('div', $options, true); + if (!$div) { + return []; } - foreach ($this->_unlockedFields as $unlockField) { - $unlockParts = explode('.', $unlockField); - if (array_values(array_intersect($field, $unlockParts)) === $unlockParts) { - return; - } + $divOptions = ['class' => 'input']; + $divOptions = $this->addClass($divOptions, $options['type']); + if (is_string($div)) { + $divOptions['class'] = $div; + } else if (is_array($div)) { + $divOptions = array_merge($divOptions, $div); } - - $field = implode('.', $field); - $field = preg_replace('/(\.\d+)+$/', '', $field); - - if ($lock) { - if (!in_array($field, $this->fields)) { - if ($value !== null) { - return $this->fields[$field] = $value; - } elseif (isset($this->fields[$field]) && $value === null) { - unset($this->fields[$field]); - } - $this->fields[] = $field; - } - } else { - $this->unlockField($field); + if ($this->_extractOption('required', $options) !== false && + $this->_introspectModel($this->model(), 'validates', $this->field()) + ) { + $divOptions = $this->addClass($divOptions, 'required'); + } + if (!isset($divOptions['tag'])) { + $divOptions['tag'] = 'div'; } + return $divOptions; } /** - * Returns true if there is an error for the given field, otherwise false + * Extracts a single option from an options array. * - * @param string $field This should be "Modelname.fieldname" - * @return bool If there are errors this method returns true, else false. - * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/form.html#FormHelper::isFieldError + * @param string $name The name of the option to pull out. + * @param array $options The array of options you want to extract. + * @param mixed $default The default option value + * @return mixed the contents of the option or default */ - public function isFieldError($field) { - $this->setEntity($field); - return (bool)$this->tagIsInvalid(); - } - + protected function _extractOption($name, $options, $default = null) + { + if (array_key_exists($name, $options)) { + return $options[$name]; + } + return $default; + } + /** - * Returns a formatted error message for given FORM field, NULL if no errors. - * - * ### Options: - * - * - `escape` boolean - Whether or not to html escape the contents of the error. - * - `wrap` mixed - Whether or not the error message should be wrapped in a div. If a - * string, will be used as the HTML tag to use. - * - `class` string - The class name for the error message + * Generate label for input * - * @param string $field A field name, like "Modelname.fieldname" - * @param string|array $text Error message as string or array of messages. - * If array contains `attributes` key it will be used as options for error container - * @param array $options Rendering options for
wrapper tag - * @return string|null If there are errors this method returns an error message, otherwise null. - * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/form.html#FormHelper::error + * @param string $fieldName Field name. + * @param array $options Options list. + * @return bool|string false or Generated label element */ - public function error($field, $text = null, $options = array()) { - $defaults = array('wrap' => true, 'class' => 'error-message', 'escape' => true); - $options += $defaults; - $this->setEntity($field); - - $error = $this->tagIsInvalid(); - if ($error === false) { - return null; + protected function _getLabel($fieldName, $options) + { + if ($options['type'] === 'radio') { + return false; } - if (is_array($text)) { - if (isset($text['attributes']) && is_array($text['attributes'])) { - $options = array_merge($options, $text['attributes']); - unset($text['attributes']); - } - $tmp = array(); - foreach ($error as &$e) { - if (isset($text[$e])) { - $tmp[] = $text[$e]; - } else { - $tmp[] = $e; - } - } - $text = $tmp; + + $label = null; + if (isset($options['label'])) { + $label = $options['label']; } - if ($text !== null) { - $error = $text; + if ($label === false) { + return false; } - if (is_array($error)) { - foreach ($error as &$e) { - if (is_numeric($e)) { - $e = __d('cake', 'Error in field %s', Inflector::humanize($this->field())); - } + return $this->_inputLabel($fieldName, $label, $options); + } + + /** + * Generate a label for an input() call. + * + * $options can contain a hash of id overrides. These overrides will be + * used instead of the generated values if present. + * + * @param string $fieldName Field name. + * @param string|array $label Label text or array with text and options. + * @param array $options Options for the label element. 'NONE' option is + * deprecated and will be removed in 3.0 + * @return string Generated label element + */ + protected function _inputLabel($fieldName, $label, $options) + { + $labelAttributes = $this->domId([], 'for'); + $idKey = null; + if ($options['type'] === 'date' || $options['type'] === 'datetime') { + $firstInput = 'M'; + if (array_key_exists('dateFormat', $options) && + ($options['dateFormat'] === null || $options['dateFormat'] === 'NONE') + ) { + $firstInput = 'H'; + } else if (!empty($options['dateFormat'])) { + $firstInput = substr($options['dateFormat'], 0, 1); + } + switch ($firstInput) { + case 'D': + $idKey = 'day'; + $labelAttributes['for'] .= 'Day'; + break; + case 'Y': + $idKey = 'year'; + $labelAttributes['for'] .= 'Year'; + break; + case 'M': + $idKey = 'month'; + $labelAttributes['for'] .= 'Month'; + break; + case 'H': + $idKey = 'hour'; + $labelAttributes['for'] .= 'Hour'; } } - if ($options['escape']) { - $error = h($error); - unset($options['escape']); + if ($options['type'] === 'time') { + $labelAttributes['for'] .= 'Hour'; + $idKey = 'hour'; } - if (is_array($error)) { - if (count($error) > 1) { - $listParams = array(); - if (isset($options['listOptions'])) { - if (is_string($options['listOptions'])) { - $listParams[] = $options['listOptions']; - } else { - if (isset($options['listOptions']['itemOptions'])) { - $listParams[] = $options['listOptions']['itemOptions']; - unset($options['listOptions']['itemOptions']); - } else { - $listParams[] = array(); - } - if (isset($options['listOptions']['tag'])) { - $listParams[] = $options['listOptions']['tag']; - unset($options['listOptions']['tag']); - } - array_unshift($listParams, $options['listOptions']); - } - unset($options['listOptions']); - } - array_unshift($listParams, $error); - $error = call_user_func_array(array($this->Html, 'nestedList'), $listParams); - } else { - $error = array_pop($error); + if (isset($idKey) && isset($options['id']) && isset($options['id'][$idKey])) { + $labelAttributes['for'] = $options['id'][$idKey]; + } + + if (is_array($label)) { + $labelText = null; + if (isset($label['text'])) { + $labelText = $label['text']; + unset($label['text']); } + $labelAttributes = array_merge($labelAttributes, $label); + } else { + $labelText = $label; } - if ($options['wrap']) { - $tag = is_string($options['wrap']) ? $options['wrap'] : 'div'; - unset($options['wrap']); - return $this->Html->tag($tag, $error, $options); + + if (isset($options['id']) && is_string($options['id'])) { + $labelAttributes = array_merge($labelAttributes, ['for' => $options['id']]); } - return $error; + return $this->label($fieldName, $labelText, $labelAttributes); } /** @@ -857,7 +879,7 @@ public function error($field, $text = null, $options = array()) { * * ``` * echo $this->Form->label('Post.published', 'Publish', array( - * 'for' => 'post-publish' + * 'for' => 'post-publish' * )); * * ``` @@ -874,7 +896,8 @@ public function error($field, $text = null, $options = array()) { * @return string The formatted LABEL element * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/form.html#FormHelper::label */ - public function label($fieldName = null, $text = null, $options = array()) { + public function label($fieldName = null, $text = null, $options = []) + { if ($fieldName === null) { $fieldName = implode('.', $this->entity()); } @@ -893,7 +916,7 @@ public function label($fieldName = null, $text = null, $options = array()) { } if (is_string($options)) { - $options = array('class' => $options); + $options = ['class' => $options]; } if (isset($options['for'])) { @@ -907,220 +930,115 @@ public function label($fieldName = null, $text = null, $options = array()) { } /** - * Generate a set of inputs for `$fields`. If $fields is null the fields of current model - * will be used. + * Generate format options * - * You can customize individual inputs through `$fields`. - * ``` - * $this->Form->inputs(array( - * 'name' => array('label' => 'custom label') - * )); - * ``` + * @param array $options Options list. + * @return array + */ + protected function _getFormat($options) + { + if ($options['type'] === 'hidden') { + return ['input']; + } + if (is_array($options['format']) && in_array('input', $options['format'])) { + return $options['format']; + } + if ($options['type'] === 'checkbox') { + return ['before', 'input', 'between', 'label', 'after', 'error']; + } + return ['before', 'label', 'between', 'input', 'after', 'error']; + } + + /** + * Returns a formatted error message for given FORM field, NULL if no errors. * - * In addition to controller fields output, `$fields` can be used to control legend - * and fieldset rendering. - * `$this->Form->inputs('My legend');` Would generate an input set with a custom legend. - * Passing `fieldset` and `legend` key in `$fields` array has been deprecated since 2.3, - * for more fine grained control use the `fieldset` and `legend` keys in `$options` param. + * ### Options: * - * @param array $fields An array of fields to generate inputs for, or null. - * @param array $blacklist A simple array of fields to not create inputs for. - * @param array $options Options array. Valid keys are: - * - `fieldset` Set to false to disable the fieldset. If a string is supplied it will be used as - * the class name for the fieldset element. - * - `legend` Set to false to disable the legend for the generated input set. Or supply a string - * to customize the legend text. - * @return string Completed form inputs. - * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/form.html#FormHelper::inputs + * - `escape` boolean - Whether or not to html escape the contents of the error. + * - `wrap` mixed - Whether or not the error message should be wrapped in a div. If a + * string, will be used as the HTML tag to use. + * - `class` string - The class name for the error message + * + * @param string $field A field name, like "Modelname.fieldname" + * @param string|array $text Error message as string or array of messages. + * If array contains `attributes` key it will be used as options for error container + * @param array $options Rendering options for
wrapper tag + * @return string|null If there are errors this method returns an error message, otherwise null. + * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/form.html#FormHelper::error */ - public function inputs($fields = null, $blacklist = null, $options = array()) { - $fieldset = $legend = true; - $modelFields = array(); - $model = $this->model(); - if ($model) { - $modelFields = array_keys((array)$this->_introspectModel($model, 'fields')); - } - if (is_array($fields)) { - if (array_key_exists('legend', $fields) && !in_array('legend', $modelFields)) { - $legend = $fields['legend']; - unset($fields['legend']); - } + public function error($field, $text = null, $options = []) + { + $defaults = ['wrap' => true, 'class' => 'error-message', 'escape' => true]; + $options += $defaults; + $this->setEntity($field); - if (isset($fields['fieldset']) && !in_array('fieldset', $modelFields)) { - $fieldset = $fields['fieldset']; - unset($fields['fieldset']); + $error = $this->tagIsInvalid(); + if ($error === false) { + return null; + } + if (is_array($text)) { + if (isset($text['attributes']) && is_array($text['attributes'])) { + $options = array_merge($options, $text['attributes']); + unset($text['attributes']); } - } elseif ($fields !== null) { - $fieldset = $legend = $fields; - if (!is_bool($fieldset)) { - $fieldset = true; + $tmp = []; + foreach ($error as &$e) { + if (isset($text[$e])) { + $tmp[] = $text[$e]; + } else { + $tmp[] = $e; + } } - $fields = array(); - } - - if (isset($options['legend'])) { - $legend = $options['legend']; - unset($options['legend']); - } - - if (isset($options['fieldset'])) { - $fieldset = $options['fieldset']; - unset($options['fieldset']); + $text = $tmp; } - if (empty($fields)) { - $fields = $modelFields; + if ($text !== null) { + $error = $text; } - - if ($legend === true) { - $actionName = __d('cake', 'New %s'); - $isEdit = ( - strpos($this->request->params['action'], 'update') !== false || - strpos($this->request->params['action'], 'edit') !== false - ); - if ($isEdit) { - $actionName = __d('cake', 'Edit %s'); + if (is_array($error)) { + foreach ($error as &$e) { + if (is_numeric($e)) { + $e = __d('cake', 'Error in field %s', Inflector::humanize($this->field())); + } } - $modelName = Inflector::humanize(Inflector::underscore($model)); - $legend = sprintf($actionName, __($modelName)); } - - $out = null; - foreach ($fields as $name => $options) { - if (is_numeric($name) && !is_array($options)) { - $name = $options; - $options = array(); - } - $entity = explode('.', $name); - $blacklisted = ( - is_array($blacklist) && - (in_array($name, $blacklist) || in_array(end($entity), $blacklist)) - ); - if ($blacklisted) { - continue; - } - $out .= $this->input($name, $options); - } - - if (is_string($fieldset)) { - $fieldsetClass = array('class' => $fieldset); - } else { - $fieldsetClass = ''; - } - - if ($fieldset) { - if ($legend) { - $out = $this->Html->useTag('legend', $legend) . $out; - } - $out = $this->Html->useTag('fieldset', $fieldsetClass, $out); - } - return $out; - } - - /** - * Generates a form input element complete with label and wrapper div - * - * ### Options - * - * See each field type method for more information. Any options that are part of - * $attributes or $options for the different **type** methods can be included in `$options` for input().i - * Additionally, any unknown keys that are not in the list below, or part of the selected type's options - * will be treated as a regular html attribute for the generated input. - * - * - `type` - Force the type of widget you want. e.g. `type => 'select'` - * - `label` - Either a string label, or an array of options for the label. See FormHelper::label(). - * - `div` - Either `false` to disable the div, or an array of options for the div. - * See HtmlHelper::div() for more options. - * - `options` - For widgets that take options e.g. radio, select. - * - `error` - Control the error message that is produced. Set to `false` to disable any kind of error reporting (field - * error and error messages). - * - `errorMessage` - Boolean to control rendering error messages (field error will still occur). - * - `empty` - String or boolean to enable empty select box options. - * - `before` - Content to place before the label + input. - * - `after` - Content to place after the label + input. - * - `between` - Content to place between the label + input. - * - `format` - Format template for element order. Any element that is not in the array, will not be in the output. - * - Default input format order: array('before', 'label', 'between', 'input', 'after', 'error') - * - Default checkbox format order: array('before', 'input', 'between', 'label', 'after', 'error') - * - Hidden input will not be formatted - * - Radio buttons cannot have the order of input and label elements controlled with these settings. - * - * @param string $fieldName This should be "Modelname.fieldname" - * @param array $options Each type of input takes different options. - * @return string Completed form widget. - * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/form.html#creating-form-elements - */ - public function input($fieldName, $options = array()) { - $this->setEntity($fieldName); - $options = $this->_parseOptions($options); - - $divOptions = $this->_divOptions($options); - unset($options['div']); - - if ($options['type'] === 'radio' && isset($options['options'])) { - $radioOptions = (array)$options['options']; - unset($options['options']); - } else { - $radioOptions = array(); - } - - $label = $this->_getLabel($fieldName, $options); - if ($options['type'] !== 'radio') { - unset($options['label']); - } - - $error = $this->_extractOption('error', $options, null); - unset($options['error']); - - $errorMessage = $this->_extractOption('errorMessage', $options, true); - unset($options['errorMessage']); - - $selected = $this->_extractOption('selected', $options, null); - unset($options['selected']); - - if ($options['type'] === 'datetime' || $options['type'] === 'date' || $options['type'] === 'time') { - $dateFormat = $this->_extractOption('dateFormat', $options, 'MDY'); - $timeFormat = $this->_extractOption('timeFormat', $options, 12); - unset($options['dateFormat'], $options['timeFormat']); - } else { - $dateFormat = 'MDY'; - $timeFormat = 12; + if ($options['escape']) { + $error = h($error); + unset($options['escape']); } - - $type = $options['type']; - $out = array('before' => $options['before'], 'label' => $label, 'between' => $options['between'], 'after' => $options['after']); - $format = $this->_getFormat($options); - - unset($options['type'], $options['before'], $options['between'], $options['after'], $options['format']); - - $out['error'] = null; - if ($type !== 'hidden' && $error !== false) { - $errMsg = $this->error($fieldName, $error); - if ($errMsg) { - $divOptions = $this->addClass($divOptions, Hash::get($divOptions, 'errorClass', 'error')); - if ($errorMessage) { - $out['error'] = $errMsg; + if (is_array($error)) { + if (count($error) > 1) { + $listParams = []; + if (isset($options['listOptions'])) { + if (is_string($options['listOptions'])) { + $listParams[] = $options['listOptions']; + } else { + if (isset($options['listOptions']['itemOptions'])) { + $listParams[] = $options['listOptions']['itemOptions']; + unset($options['listOptions']['itemOptions']); + } else { + $listParams[] = []; + } + if (isset($options['listOptions']['tag'])) { + $listParams[] = $options['listOptions']['tag']; + unset($options['listOptions']['tag']); + } + array_unshift($listParams, $options['listOptions']); + } + unset($options['listOptions']); } + array_unshift($listParams, $error); + $error = call_user_func_array([$this->Html, 'nestedList'], $listParams); + } else { + $error = array_pop($error); } } - - if ($type === 'radio' && isset($out['between'])) { - $options['between'] = $out['between']; - $out['between'] = null; - } - $out['input'] = $this->_getInput(compact('type', 'fieldName', 'options', 'radioOptions', 'selected', 'dateFormat', 'timeFormat')); - - $output = ''; - foreach ($format as $element) { - $output .= $out[$element]; - } - - if (!empty($divOptions['tag'])) { - $tag = $divOptions['tag']; - unset($divOptions['tag'], $divOptions['errorClass']); - $output = $this->Html->tag($tag, $output, $divOptions); + if ($options['wrap']) { + $tag = is_string($options['wrap']) ? $options['wrap'] : 'div'; + unset($options['wrap']); + return $this->Html->tag($tag, $error, $options); } - return $output; + return $error; } /** @@ -1129,7 +1047,8 @@ public function input($fieldName, $options = array()) { * @param array $args The options for the input element * @return string The generated input element */ - protected function _getInput($args) { + protected function _getInput($args) + { extract($args); switch ($type) { case 'hidden': @@ -1141,345 +1060,208 @@ protected function _getInput($args) { case 'file': return $this->file($fieldName, $options); case 'select': - $options += array('options' => array(), 'value' => $selected); + $options += ['options' => [], 'value' => $selected]; $list = $options['options']; unset($options['options']); return $this->select($fieldName, $list, $options); case 'time': - $options += array('value' => $selected); + $options += ['value' => $selected]; return $this->dateTime($fieldName, null, $timeFormat, $options); case 'date': - $options += array('value' => $selected); + $options += ['value' => $selected]; return $this->dateTime($fieldName, $dateFormat, null, $options); case 'datetime': - $options += array('value' => $selected); + $options += ['value' => $selected]; return $this->dateTime($fieldName, $dateFormat, $timeFormat, $options); case 'textarea': - return $this->textarea($fieldName, $options + array('cols' => '30', 'rows' => '6')); + return $this->textarea($fieldName, $options + ['cols' => '30', 'rows' => '6']); case 'url': - return $this->text($fieldName, array('type' => 'url') + $options); + return $this->text($fieldName, ['type' => 'url'] + $options); default: return $this->{$type}($fieldName, $options); } } /** - * Generates input options array + * Creates a hidden input field. * - * @param array $options Options list. - * @return array Options + * @param string $fieldName Name of a field, in the form of "Modelname.fieldname" + * @param array $options Array of HTML attributes. + * @return string A generated hidden input + * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/form.html#FormHelper::hidden */ - protected function _parseOptions($options) { - $options = array_merge( - array('before' => null, 'between' => null, 'after' => null, 'format' => null), - $this->_inputDefaults, - $options - ); - - if (!isset($options['type'])) { - $options = $this->_magicOptions($options); - } + public function hidden($fieldName, $options = []) + { + $options += ['required' => false, 'secure' => true]; - if (in_array($options['type'], array('radio', 'select'))) { - $options = $this->_optionsOptions($options); - } + $secure = $options['secure']; + unset($options['secure']); - $options = $this->_maxLength($options); + $options = $this->_initInputField($fieldName, array_merge( + $options, ['secure' => static::SECURE_SKIP] + )); - if (isset($options['rows']) || isset($options['cols'])) { - $options['type'] = 'textarea'; + if ($secure === true) { + $this->_secure(true, null, '' . $options['value']); } - if ($options['type'] === 'datetime' || $options['type'] === 'date' || $options['type'] === 'time' || $options['type'] === 'select') { - $options += array('empty' => false); - } - return $options; + return $this->Html->useTag('hidden', $options['name'], array_diff_key($options, ['name' => null])); } /** - * Generates list of options for multiple select + * Sets field defaults and adds field to form security input hash. + * Will also add a 'form-error' class if the field contains validation errors. * - * @param array $options Options list. - * @return array + * ### Options + * + * - `secure` - boolean whether or not the field should be added to the security fields. + * Disabling the field using the `disabled` option, will also omit the field from being + * part of the hashed key. + * + * This method will convert a numerically indexed 'disabled' into an associative + * value. FormHelper's internals expect associative options. + * + * @param string $field Name of the field to initialize options for. + * @param array $options Array of options to append options into. + * @return array Array of options for the input. */ - protected function _optionsOptions($options) { - if (isset($options['options'])) { - return $options; + protected function _initInputField($field, $options = []) + { + if (isset($options['secure'])) { + $secure = $options['secure']; + unset($options['secure']); + } else { + $secure = (isset($this->request['_Token']) && !empty($this->request['_Token'])); } - $varName = Inflector::variable( - Inflector::pluralize(preg_replace('/_id$/', '', $this->field())) - ); - $varOptions = $this->_View->get($varName); - if (!is_array($varOptions)) { - return $options; + + $disabledIndex = array_search('disabled', $options, true); + if (is_int($disabledIndex)) { + unset($options[$disabledIndex]); + $options['disabled'] = true; } - if ($options['type'] !== 'radio') { - $options['type'] = 'select'; + + $result = parent::_initInputField($field, $options); + if ($this->tagIsInvalid() !== false) { + $result = $this->addClass($result, 'form-error'); } - $options['options'] = $varOptions; - return $options; + + $isDisabled = false; + if (isset($result['disabled'])) { + $isDisabled = ( + $result['disabled'] === true || + $result['disabled'] === 'disabled' || + (is_array($result['disabled']) && + !empty($result['options']) && + array_diff($result['options'], $result['disabled']) === [] + ) + ); + } + if ($isDisabled) { + return $result; + } + + if (!isset($result['required']) && + $this->_introspectModel($this->model(), 'validates', $this->field()) + ) { + $result['required'] = true; + } + + if ($secure === static::SECURE_SKIP) { + return $result; + } + + $this->_secure($secure, $this->_secureFieldName($options)); + return $result; } /** - * Magically set option type and corresponding options + * Determine which fields of a form should be used for hash. + * Populates $this->fields * - * @param array $options Options list. - * @return array - */ - protected function _magicOptions($options) { - $modelKey = $this->model(); - $fieldKey = $this->field(); - $options['type'] = 'text'; - if (isset($options['options'])) { - $options['type'] = 'select'; - } elseif (in_array($fieldKey, array('psword', 'passwd', 'password'))) { - $options['type'] = 'password'; - } elseif (in_array($fieldKey, array('tel', 'telephone', 'phone'))) { - $options['type'] = 'tel'; - } elseif ($fieldKey === 'email') { - $options['type'] = 'email'; - } elseif (isset($options['checked'])) { - $options['type'] = 'checkbox'; - } elseif ($fieldDef = $this->_introspectModel($modelKey, 'fields', $fieldKey)) { - $type = $fieldDef['type']; - $primaryKey = $this->fieldset[$modelKey]['key']; - $map = array( - 'string' => 'text', - 'datetime' => 'datetime', - 'boolean' => 'checkbox', - 'timestamp' => 'datetime', - 'text' => 'textarea', - 'time' => 'time', - 'date' => 'date', - 'float' => 'number', - 'integer' => 'number', - 'smallinteger' => 'number', - 'tinyinteger' => 'number', - 'decimal' => 'number', - 'binary' => 'file' - ); - - if (isset($this->map[$type])) { - $options['type'] = $this->map[$type]; - } elseif (isset($map[$type])) { - $options['type'] = $map[$type]; - } - if ($fieldKey === $primaryKey) { - $options['type'] = 'hidden'; - } - if ($options['type'] === 'number' && - !isset($options['step']) - ) { - if ($type === 'decimal' && isset($fieldDef['length'])) { - $decimalPlaces = substr($fieldDef['length'], strpos($fieldDef['length'], ',') + 1); - $options['step'] = sprintf('%.' . $decimalPlaces . 'F', pow(10, -1 * $decimalPlaces)); - } elseif ($type === 'float' || $type === 'decimal') { - $options['step'] = 'any'; - } - } - } - - if (preg_match('/_id$/', $fieldKey) && $options['type'] !== 'hidden') { - $options['type'] = 'select'; - } - - if ($modelKey === $fieldKey) { - $options['type'] = 'select'; - if (!isset($options['multiple'])) { - $options['multiple'] = 'multiple'; - } - } - if (in_array($options['type'], array('text', 'number'))) { - $options = $this->_optionsOptions($options); - } - if ($options['type'] === 'select' && array_key_exists('step', $options)) { - unset($options['step']); - } - - return $options; - } - - /** - * Generate format options - * - * @param array $options Options list. - * @return array + * @param bool $lock Whether this field should be part of the validation + * or excluded as part of the unlockedFields. + * @param string|array $field Reference to field to be secured. Should be dot separated to indicate nesting. + * @param mixed $value Field value, if value should not be tampered with. + * @return void */ - protected function _getFormat($options) { - if ($options['type'] === 'hidden') { - return array('input'); - } - if (is_array($options['format']) && in_array('input', $options['format'])) { - return $options['format']; - } - if ($options['type'] === 'checkbox') { - return array('before', 'input', 'between', 'label', 'after', 'error'); + protected function _secure($lock, $field = null, $value = null) + { + if (!$field) { + $field = $this->entity(); + } else if (is_string($field)) { + $field = explode('.', $field); } - return array('before', 'label', 'between', 'input', 'after', 'error'); - } - - /** - * Generate label for input - * - * @param string $fieldName Field name. - * @param array $options Options list. - * @return bool|string false or Generated label element - */ - protected function _getLabel($fieldName, $options) { - if ($options['type'] === 'radio') { - return false; + if (is_array($field)) { + $field = Hash::filter($field); } - $label = null; - if (isset($options['label'])) { - $label = $options['label']; + foreach ($this->_unlockedFields as $unlockField) { + $unlockParts = explode('.', $unlockField); + if (array_values(array_intersect($field, $unlockParts)) === $unlockParts) { + return; + } } - if ($label === false) { - return false; - } - return $this->_inputLabel($fieldName, $label, $options); - } + $field = implode('.', $field); + $field = preg_replace('/(\.\d+)+$/', '', $field); - /** - * Calculates maxlength option - * - * @param array $options Options list. - * @return array - */ - protected function _maxLength($options) { - $fieldDef = $this->_introspectModel($this->model(), 'fields', $this->field()); - $autoLength = ( - !array_key_exists('maxlength', $options) && - isset($fieldDef['length']) && - is_scalar($fieldDef['length']) && - $fieldDef['length'] < 1000000 && - $fieldDef['type'] !== 'decimal' && - $fieldDef['type'] !== 'time' && - $fieldDef['type'] !== 'datetime' && - $options['type'] !== 'select' - ); - if ($autoLength && - in_array($options['type'], array('text', 'textarea', 'email', 'tel', 'url', 'search')) - ) { - $options['maxlength'] = (int)$fieldDef['length']; + if ($lock) { + if (!in_array($field, $this->fields)) { + if ($value !== null) { + return $this->fields[$field] = $value; + } else if (isset($this->fields[$field]) && $value === null) { + unset($this->fields[$field]); + } + $this->fields[] = $field; + } + } else { + $this->unlockField($field); } - return $options; } /** - * Generate div options for input + * Add to or get the list of fields that are currently unlocked. + * Unlocked fields are not included in the field hash used by SecurityComponent + * unlocking a field once its been added to the list of secured fields will remove + * it from the list of fields. * - * @param array $options Options list. - * @return array + * @param string $name The dot separated name for the field. + * @return mixed Either null, or the list of fields. + * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/form.html#FormHelper::unlockField */ - protected function _divOptions($options) { - if ($options['type'] === 'hidden') { - return array(); - } - $div = $this->_extractOption('div', $options, true); - if (!$div) { - return array(); - } - - $divOptions = array('class' => 'input'); - $divOptions = $this->addClass($divOptions, $options['type']); - if (is_string($div)) { - $divOptions['class'] = $div; - } elseif (is_array($div)) { - $divOptions = array_merge($divOptions, $div); - } - if ($this->_extractOption('required', $options) !== false && - $this->_introspectModel($this->model(), 'validates', $this->field()) - ) { - $divOptions = $this->addClass($divOptions, 'required'); + public function unlockField($name = null) + { + if ($name === null) { + return $this->_unlockedFields; } - if (!isset($divOptions['tag'])) { - $divOptions['tag'] = 'div'; + if (!in_array($name, $this->_unlockedFields)) { + $this->_unlockedFields[] = $name; } - return $divOptions; - } - - /** - * Extracts a single option from an options array. - * - * @param string $name The name of the option to pull out. - * @param array $options The array of options you want to extract. - * @param mixed $default The default option value - * @return mixed the contents of the option or default - */ - protected function _extractOption($name, $options, $default = null) { - if (array_key_exists($name, $options)) { - return $options[$name]; + $index = array_search($name, $this->fields); + if ($index !== false) { + unset($this->fields[$index]); } - return $default; + unset($this->fields[$name]); } /** - * Generate a label for an input() call. + * Get the field name for use with _secure(). * - * $options can contain a hash of id overrides. These overrides will be - * used instead of the generated values if present. + * Parses the name attribute to create a dot separated name value for use + * in secured field hash. * - * @param string $fieldName Field name. - * @param string|array $label Label text or array with text and options. - * @param array $options Options for the label element. 'NONE' option is - * deprecated and will be removed in 3.0 - * @return string Generated label element + * @param array $options An array of options possibly containing a name key. + * @return string|null */ - protected function _inputLabel($fieldName, $label, $options) { - $labelAttributes = $this->domId(array(), 'for'); - $idKey = null; - if ($options['type'] === 'date' || $options['type'] === 'datetime') { - $firstInput = 'M'; - if (array_key_exists('dateFormat', $options) && - ($options['dateFormat'] === null || $options['dateFormat'] === 'NONE') - ) { - $firstInput = 'H'; - } elseif (!empty($options['dateFormat'])) { - $firstInput = substr($options['dateFormat'], 0, 1); - } - switch ($firstInput) { - case 'D': - $idKey = 'day'; - $labelAttributes['for'] .= 'Day'; - break; - case 'Y': - $idKey = 'year'; - $labelAttributes['for'] .= 'Year'; - break; - case 'M': - $idKey = 'month'; - $labelAttributes['for'] .= 'Month'; - break; - case 'H': - $idKey = 'hour'; - $labelAttributes['for'] .= 'Hour'; - } - } - if ($options['type'] === 'time') { - $labelAttributes['for'] .= 'Hour'; - $idKey = 'hour'; - } - if (isset($idKey) && isset($options['id']) && isset($options['id'][$idKey])) { - $labelAttributes['for'] = $options['id'][$idKey]; - } - - if (is_array($label)) { - $labelText = null; - if (isset($label['text'])) { - $labelText = $label['text']; - unset($label['text']); + protected function _secureFieldName($options) + { + if (isset($options['name'])) { + preg_match_all('/\[(.*?)\]/', $options['name'], $matches); + if (isset($matches[1])) { + return $matches[1]; } - $labelAttributes = array_merge($labelAttributes, $label); - } else { - $labelText = $label; - } - - if (isset($options['id']) && is_string($options['id'])) { - $labelAttributes = array_merge($labelAttributes, array('for' => $options['id'])); } - return $this->label($fieldName, $labelText, $labelAttributes); + return null; } /** @@ -1501,15 +1283,16 @@ protected function _inputLabel($fieldName, $label, $options) { * @return string An HTML text input element. * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/form.html#options-for-select-checkbox-and-radio-inputs */ - public function checkbox($fieldName, $options = array()) { - $valueOptions = array(); + public function checkbox($fieldName, $options = []) + { + $valueOptions = []; if (isset($options['default'])) { $valueOptions['default'] = $options['default']; unset($options['default']); } - $options += array('value' => 1, 'required' => false); - $options = $this->_initInputField($fieldName, $options) + array('hiddenField' => true); + $options += ['value' => 1, 'required' => false]; + $options = $this->_initInputField($fieldName, $options) + ['hiddenField' => true]; $value = current($this->value($valueOptions)); $output = ''; @@ -1519,13 +1302,13 @@ public function checkbox($fieldName, $options = array()) { $options['checked'] = 'checked'; } if ($options['hiddenField']) { - $hiddenOptions = array( + $hiddenOptions = [ 'id' => $options['id'] . '_', 'name' => $options['name'], 'value' => ($options['hiddenField'] !== true ? $options['hiddenField'] : '0'), 'form' => isset($options['form']) ? $options['form'] : null, 'secure' => false, - ); + ]; if (isset($options['disabled']) && $options['disabled']) { $hiddenOptions['disabled'] = 'disabled'; } @@ -1533,7 +1316,7 @@ public function checkbox($fieldName, $options = array()) { } unset($options['hiddenField']); - return $output . $this->Html->useTag('checkbox', $options['name'], array_diff_key($options, array('name' => null))); + return $output . $this->Html->useTag('checkbox', $options['name'], array_diff_key($options, ['name' => null])); } /** @@ -1570,7 +1353,8 @@ public function checkbox($fieldName, $options = array()) { * @return string Completed radio widget set. * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/form.html#options-for-select-checkbox-and-radio-inputs */ - public function radio($fieldName, $options = array(), $attributes = array()) { + public function radio($fieldName, $options = [], $attributes = []) + { $attributes['options'] = $options; $attributes = $this->_initInputField($fieldName, $attributes); unset($attributes['options']); @@ -1578,7 +1362,7 @@ public function radio($fieldName, $options = array(), $attributes = array()) { $showEmpty = $this->_extractOption('empty', $attributes); if ($showEmpty) { $showEmpty = ($showEmpty === true) ? __d('cake', 'empty') : $showEmpty; - $options = array('' => $showEmpty) + $options; + $options = ['' => $showEmpty] + $options; } unset($attributes['empty']); @@ -1586,13 +1370,13 @@ public function radio($fieldName, $options = array(), $attributes = array()) { if (isset($attributes['legend'])) { $legend = $attributes['legend']; unset($attributes['legend']); - } elseif (count($options) > 1) { + } else if (count($options) > 1) { $legend = __(Inflector::humanize($this->field())); } $fieldsetAttrs = ''; if (isset($attributes['fieldset'])) { - $fieldsetAttrs = array('class' => $attributes['fieldset']); + $fieldsetAttrs = ['class' => $attributes['fieldset']]; unset($attributes['fieldset']); } @@ -1621,12 +1405,12 @@ public function radio($fieldName, $options = array(), $attributes = array()) { $value = $this->value($fieldName); } - $disabled = array(); + $disabled = []; if (isset($attributes['disabled'])) { $disabled = $attributes['disabled']; } - $out = array(); + $out = []; $hiddenField = isset($attributes['hiddenField']) ? $attributes['hiddenField'] : true; unset($attributes['hiddenField']); @@ -1635,9 +1419,9 @@ public function radio($fieldName, $options = array(), $attributes = array()) { $value = $value ? 1 : 0; } - $this->_domIdSuffixes = array(); + $this->_domIdSuffixes = []; foreach ($options as $optValue => $optTitle) { - $optionsHere = array('value' => $optValue, 'disabled' => false); + $optionsHere = ['value' => $optValue, 'disabled' => false]; if (is_array($optTitle)) { if (isset($optTitle['value'])) { $optionsHere['value'] = $optTitle['value']; @@ -1658,8 +1442,8 @@ public function radio($fieldName, $options = array(), $attributes = array()) { $tagName = $attributes['id'] . $this->domIdSuffix($optValue); if ($label) { - $labelOpts = is_array($label) ? $label : array(); - $labelOpts += array('for' => $tagName); + $labelOpts = is_array($label) ? $label : []; + $labelOpts += ['for' => $tagName]; $optTitle = $this->label($tagName, $optTitle, $labelOpts); } @@ -1668,7 +1452,7 @@ public function radio($fieldName, $options = array(), $attributes = array()) { } $allOptions = $optionsHere + $attributes; $out[] = $this->Html->useTag('radio', $attributes['name'], $tagName, - array_diff_key($allOptions, array('name' => null, 'type' => null, 'id' => null)), + array_diff_key($allOptions, ['name' => null, 'type' => null, 'id' => null]), $optTitle ); } @@ -1676,12 +1460,12 @@ public function radio($fieldName, $options = array(), $attributes = array()) { if ($hiddenField) { if (!isset($value) || $value === '') { - $hidden = $this->hidden($fieldName, array( + $hidden = $this->hidden($fieldName, [ 'form' => isset($attributes['form']) ? $attributes['form'] : null, 'id' => $attributes['id'] . '_', 'value' => $hiddenField === true ? '' : $hiddenField, 'name' => $attributes['name'] - )); + ]); } } $out = $hidden . implode($separator, $out); @@ -1698,90 +1482,32 @@ public function radio($fieldName, $options = array(), $attributes = array()) { } /** - * Missing method handler - implements various simple input types. Is used to create inputs - * of various types. e.g. `$this->Form->text();` will create `` while - * `$this->Form->range();` will create `` - * - * ### Usage - * - * `$this->Form->search('User.query', array('value' => 'test'));` - * - * Will make an input like: - * - * `` - * - * The first argument to an input type should always be the fieldname, in `Model.field` format. - * The second argument should always be an array of attributes for the input. - * - * @param string $method Method name / input type to make. - * @param array $params Parameters for the method call - * @return string Formatted input method. - * @throws CakeException When there are no params for the method call. - */ - public function __call($method, $params) { - $options = array(); - if (empty($params)) { - throw new CakeException(__d('cake_dev', 'Missing field name for FormHelper::%s', $method)); - } - if (isset($params[1])) { - $options = $params[1]; - } - if (!isset($options['type'])) { - $options['type'] = $method; - } - $options = $this->_initInputField($params[0], $options); - return $this->Html->useTag('input', $options['name'], array_diff_key($options, array('name' => null))); - } - - /** - * Creates a textarea widget. - * - * ### Options: + * Generates a valid DOM ID suffix from a string. + * Also avoids collisions when multiple values are coverted to the same suffix by + * appending a numeric value. * - * - `escape` - Whether or not the contents of the textarea should be escaped. Defaults to true. + * For pre-HTML5 IDs only characters like a-z 0-9 - _ are valid. HTML5 doesn't have that + * limitation, but to avoid layout issues it still filters out some sensitive chars. * - * @param string $fieldName Name of a field, in the form "Modelname.fieldname" - * @param array $options Array of HTML attributes, and special options above. - * @return string A generated HTML text input element - * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/form.html#FormHelper::textarea + * @param string $value The value that should be transferred into a DOM ID suffix. + * @param string $type Doctype to use. Defaults to html4. + * @return string DOM ID */ - public function textarea($fieldName, $options = array()) { - $options = $this->_initInputField($fieldName, $options); - $value = null; - - if (array_key_exists('value', $options)) { - $value = $options['value']; - if (!array_key_exists('escape', $options) || $options['escape'] !== false) { - $value = h($value); - } - unset($options['value']); + public function domIdSuffix($value, $type = 'html4') + { + if ($type === 'html5') { + $value = str_replace(['@', '<', '>', ' ', '"', '\''], '_', $value); + } else { + $value = Inflector::camelize(Inflector::slug($value)); } - return $this->Html->useTag('textarea', $options['name'], array_diff_key($options, array('type' => null, 'name' => null)), $value); - } - - /** - * Creates a hidden input field. - * - * @param string $fieldName Name of a field, in the form of "Modelname.fieldname" - * @param array $options Array of HTML attributes. - * @return string A generated hidden input - * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/form.html#FormHelper::hidden - */ - public function hidden($fieldName, $options = array()) { - $options += array('required' => false, 'secure' => true); - - $secure = $options['secure']; - unset($options['secure']); - - $options = $this->_initInputField($fieldName, array_merge( - $options, array('secure' => static::SECURE_SKIP) - )); - - if ($secure === true) { - $this->_secure(true, null, '' . $options['value']); + $value = Inflector::camelize($value); + $count = 1; + $suffix = $value; + while (in_array($suffix, $this->_domIdSuffixes)) { + $suffix = $value . $count++; } - - return $this->Html->useTag('hidden', $options['name'], array_diff_key($options, array('name' => null))); + $this->_domIdSuffixes[] = $suffix; + return $suffix; } /** @@ -1792,385 +1518,114 @@ public function hidden($fieldName, $options = array()) { * @return string A generated file input. * @link https://book.cakephp.org/2.0/en/core-libraries/helpers/form.html#FormHelper::file */ - public function file($fieldName, $options = array()) { - $options += array('secure' => true); + public function file($fieldName, $options = []) + { + $options += ['secure' => true]; $secure = $options['secure']; $options['secure'] = static::SECURE_SKIP; $options = $this->_initInputField($fieldName, $options); $field = $this->entity(); - foreach (array('name', 'type', 'tmp_name', 'error', 'size') as $suffix) { - $this->_secure($secure, array_merge($field, array($suffix))); + foreach (['name', 'type', 'tmp_name', 'error', 'size'] as $suffix) { + $this->_secure($secure, array_merge($field, [$suffix])); } - $exclude = array('name' => null, 'value' => null); + $exclude = ['name' => null, 'value' => null]; return $this->Html->useTag('file', $options['name'], array_diff_key($options, $exclude)); } /** - * Creates a `