From 6dfd5d507d9863f987b30b0a5ab4268aea2ed875 Mon Sep 17 00:00:00 2001 From: Ludovic Pouzenc Date: Thu, 2 Aug 2012 11:09:40 +0000 Subject: J'étais parti sur un download pourri de Cake. Les gars on abusé sur GitHub. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit git-svn-id: file:///var/svn/2012-php-weave/trunk@7 d972a294-176a-4cf9-8ea1-fcd5b0c30f5c --- .../lib/Cake/Model/AclNode.php | 182 ++ .../lib/Cake/Model/Aco.php | 41 + .../lib/Cake/Model/AcoAction.php | 41 + .../lib/Cake/Model/Aro.php | 41 + .../lib/Cake/Model/Behavior/AclBehavior.php | 142 + .../Cake/Model/Behavior/ContainableBehavior.php | 428 +++ .../lib/Cake/Model/Behavior/TranslateBehavior.php | 627 ++++ .../lib/Cake/Model/Behavior/TreeBehavior.php | 1007 ++++++ .../lib/Cake/Model/BehaviorCollection.php | 296 ++ .../lib/Cake/Model/CakeSchema.php | 710 ++++ .../lib/Cake/Model/ConnectionManager.php | 266 ++ .../lib/Cake/Model/Datasource/CakeSession.php | 688 ++++ .../lib/Cake/Model/Datasource/DataSource.php | 442 +++ .../lib/Cake/Model/Datasource/Database/Mysql.php | 688 ++++ .../Cake/Model/Datasource/Database/Postgres.php | 907 ++++++ .../lib/Cake/Model/Datasource/Database/Sqlite.php | 571 ++++ .../Cake/Model/Datasource/Database/Sqlserver.php | 783 +++++ .../lib/Cake/Model/Datasource/DboSource.php | 3268 +++++++++++++++++++ .../Cake/Model/Datasource/Session/CacheSession.php | 90 + .../Session/CakeSessionHandlerInterface.php | 72 + .../Model/Datasource/Session/DatabaseSession.php | 144 + .../lib/Cake/Model/I18nModel.php | 44 + .../lib/Cake/Model/Model.php | 3402 ++++++++++++++++++++ .../lib/Cake/Model/ModelBehavior.php | 236 ++ .../lib/Cake/Model/ModelValidator.php | 599 ++++ .../lib/Cake/Model/Permission.php | 257 ++ .../Cake/Model/Validator/CakeValidationRule.php | 333 ++ .../lib/Cake/Model/Validator/CakeValidationSet.php | 350 ++ 28 files changed, 16655 insertions(+) create mode 100644 poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/AclNode.php create mode 100644 poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Aco.php create mode 100644 poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/AcoAction.php create mode 100644 poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Aro.php create mode 100644 poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Behavior/AclBehavior.php create mode 100644 poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Behavior/ContainableBehavior.php create mode 100644 poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Behavior/TranslateBehavior.php create mode 100644 poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Behavior/TreeBehavior.php create mode 100644 poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/BehaviorCollection.php create mode 100644 poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/CakeSchema.php create mode 100644 poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/ConnectionManager.php create mode 100644 poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Datasource/CakeSession.php create mode 100644 poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Datasource/DataSource.php create mode 100644 poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Datasource/Database/Mysql.php create mode 100644 poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Datasource/Database/Postgres.php create mode 100644 poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Datasource/Database/Sqlite.php create mode 100644 poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Datasource/Database/Sqlserver.php create mode 100644 poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Datasource/DboSource.php create mode 100644 poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Datasource/Session/CacheSession.php create mode 100644 poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Datasource/Session/CakeSessionHandlerInterface.php create mode 100644 poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Datasource/Session/DatabaseSession.php create mode 100644 poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/I18nModel.php create mode 100644 poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Model.php create mode 100644 poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/ModelBehavior.php create mode 100644 poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/ModelValidator.php create mode 100644 poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Permission.php create mode 100644 poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Validator/CakeValidationRule.php create mode 100644 poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Validator/CakeValidationSet.php (limited to 'poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model') diff --git a/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/AclNode.php b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/AclNode.php new file mode 100644 index 0000000..ca3357c --- /dev/null +++ b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/AclNode.php @@ -0,0 +1,182 @@ + array('type' => 'nested')); + +/** + * Constructor + * + */ + public function __construct() { + $config = Configure::read('Acl.database'); + if (isset($config)) { + $this->useDbConfig = $config; + } + parent::__construct(); + } + +/** + * 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' => 'LEFT', + 'conditions' => array("{$type}0.alias" => $start) + )), + 'order' => $db->name("{$type}.lft") . ' DESC' + ); + + foreach ($path as $i => $alias) { + $j = $i - 1; + + $queryData['joins'][] = array( + 'table' => $table, + 'alias' => "{$type}{$i}", + 'type' => 'LEFT', + 'conditions' => array( + $db->name("{$type}{$i}.lft") . ' > ' . $db->name("{$type}{$j}.lft"), + $db->name("{$type}{$i}.rght") . ' < ' . $db->name("{$type}{$j}.rght"), + $db->name("{$type}{$i}.alias") . ' = ' . $db->value($alias, 'string'), + $db->name("{$type}{$j}.id") . ' = ' . $db->name("{$type}{$i}.parent_id") + ) + ); + + $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")) + ); + } + $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) && is_a($ref, '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($plugin, $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' => 'LEFT', + '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; + } + +} diff --git a/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Aco.php b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Aco.php new file mode 100644 index 0000000..0ee696e --- /dev/null +++ b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Aco.php @@ -0,0 +1,41 @@ + array('with' => 'Permission')); +} \ No newline at end of file diff --git a/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/AcoAction.php b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/AcoAction.php new file mode 100644 index 0000000..2783600 --- /dev/null +++ b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/AcoAction.php @@ -0,0 +1,41 @@ + array('with' => 'Permission')); +} diff --git a/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Behavior/AclBehavior.php b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Behavior/AclBehavior.php new file mode 100644 index 0000000..d0f0a4c --- /dev/null +++ b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Behavior/AclBehavior.php @@ -0,0 +1,142 @@ + 'Aro', 'controlled' => 'Aco', 'both' => array('Aro', 'Aco')); + +/** + * Sets up the configuration for the model, and loads ACL models if they haven't been already + * + * @param Model $model + * @param array $config + * @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']); + + $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 parentNode() not defined in %s', $model->alias), E_USER_WARNING); + } + } + +/** + * Retrieves the Aro/Aco node for this model + * + * @param Model $model + * @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 http://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 null; + } + } + 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 + * @param boolean $created True if this is a new record + * @return void + */ + public function afterSave(Model $model, $created) { + $types = $this->_typeMaps[$this->settings[$model->name]['type']]; + if (!is_array($types)) { + $types = array($types); + } + foreach ($types as $type) { + $parent = $model->parentNode(); + 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); + } + } + +/** + * Destroys the ARO/ACO node bound to the deleted record + * + * @param Model $model + * @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); + } + } + } + +} diff --git a/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Behavior/ContainableBehavior.php b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Behavior/ContainableBehavior.php new file mode 100644 index 0000000..bb33eaf --- /dev/null +++ b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Behavior/ContainableBehavior.php @@ -0,0 +1,428 @@ +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); + } + +/** + * 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 = ( + (isset($this->runtime[$Model->alias]['contain']) && empty($this->runtime[$Model->alias]['contain'])) || + (isset($query['contain']) && empty($query['contain'])) + ); + $contain = array(); + if (isset($this->runtime[$Model->alias]['contain'])) { + $contain = $this->runtime[$Model->alias]['contain']; + unset($this->runtime[$Model->alias]['contain']); + } + if (isset($query['contain'])) { + $contain = array_merge($contain, (array)$query['contain']); + } + if ( + $noContain || !$contain || in_array($contain, array(null, false), true) || + (isset($contain[0]) && $contain[0] === null) + ) { + 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 $name => $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; + } + } + } + } + } + + if ($this->settings[$Model->alias]['recursive']) { + $query['recursive'] = (isset($query['recursive'])) ? $query['recursive'] : $containments['depth']; + } + + $autoFields = ($this->settings[$Model->alias]['autoFields'] + && !in_array($Model->findQueryType, array('list', 'count')) + && !empty($query['fields'])); + + 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; + } + } + } + } + } + + 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; + } + +/** + * 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 http://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; + } + +/** + * 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); + } + } + } + +/** + * 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 boolean $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', 'deleteQuery', 'insertQuery'); + $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); + } + + $children = (array)$children; + foreach ($children as $key => $val) { + if (is_string($key) && is_string($val) && !in_array($key, $options, true)) { + $children[$key] = (array)$val; + } + } + + $keys = array_keys($children); + if ($keys && isset($children[0])) { + $keys = array_merge(array_values($children), $keys); + } + + 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]); + } + } + + 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; + } + + $containments = $this->containments($Model->{$name}, $children, $containments); + $depths[] = $containments['depth'] + 1; + if (!isset($keep[$name])) { + $keep[$name] = array(); + } + } + + if (!isset($containments['models'][$Model->alias])) { + $containments['models'][$Model->alias] = array('keep' => array(), 'instance' => &$Model); + } + + $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|boolean $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); + } + +/** + * 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; + } + +} diff --git a/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Behavior/TranslateBehavior.php b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Behavior/TranslateBehavior.php new file mode 100644 index 0000000..387a37f --- /dev/null +++ b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Behavior/TranslateBehavior.php @@ -0,0 +1,627 @@ + 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 + * + * @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()); + $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']) && 'COUNT(*) AS ' . $db->name('count') == $query['fields']) { + $query['fields'] = 'COUNT(DISTINCT(' . $db->name($model->alias . '.' . $model->primaryKey) . ')) ' . $db->alias . 'count'; + $query['joins'][] = array( + 'type' => 'INNER', + 'alias' => $RuntimeModel->alias, + 'table' => $joinTable, + 'conditions' => array( + $model->alias . '.' . $model->primaryKey => $db->identifier($RuntimeModel->alias . '.foreign_key'), + $RuntimeModel->alias . '.model' => $model->name, + $RuntimeModel->alias . '.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; + } + + $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'])) { + foreach ($fields as $key => $value) { + $field = (is_numeric($key)) ? $value : $key; + + if (in_array($model->alias . '.*', $query['fields']) || in_array($model->alias . '.' . $field, $query['fields']) || in_array($field, $query['fields'])) { + $addFields[] = $field; + } + } + } + + $this->runtime[$model->alias]['virtualFields'] = $model->virtualFields; + 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); + } + } + $this->runtime[$model->alias]['beforeFind'] = $addFields; + unset($this->_joinTable, $this->_runtimeModel); + 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) { + $conditionFields = array(); + if (empty($query['conditions']) || (!empty($query['conditions']) && !is_array($query['conditions'])) ) { + return $conditionFields; + } + foreach ($query['conditions'] as $col => $val) { + 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 and possibly a field. + * + * @param Model $model The model being worked on. + * @param object $joinTable The jointable object. + * @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. + * @param boolean $addField Whether or not to add a field. + * @return array The modfied query + */ + protected function _addJoin(Model $model, $query, $field, $aliasField, $locale, $addField = false) { + $db = ConnectionManager::getDataSource($model->useDbConfig); + + $RuntimeModel = $this->_runtimeModel; + $joinTable = $this->_joinTable; + + if (is_array($locale)) { + foreach ($locale as $_locale) { + $model->virtualFields['i18n_' . $field . '_' . $_locale] = 'I18n__' . $field . '__' . $_locale . '.content'; + if (!empty($query['fields']) && is_array($query['fields'])) { + $query['fields'][] = 'i18n_' . $field . '_' . $_locale; + } + $query['joins'][] = array( + 'type' => 'LEFT', + 'alias' => 'I18n__' . $field . '__' . $_locale, + 'table' => $joinTable, + 'conditions' => array( + $model->alias . '.' . $model->primaryKey => $db->identifier("I18n__{$field}__{$_locale}.foreign_key"), + 'I18n__' . $field . '__' . $_locale . '.model' => $model->name, + 'I18n__' . $field . '__' . $_locale . '.' . $RuntimeModel->displayField => $aliasField, + 'I18n__' . $field . '__' . $_locale . '.locale' => $_locale + ) + ); + } + } else { + $model->virtualFields['i18n_' . $field] = 'I18n__' . $field . '.content'; + if (!empty($query['fields']) && is_array($query['fields'])) { + $query['fields'][] = 'i18n_' . $field; + } + $query['joins'][] = array( + 'type' => 'INNER', + 'alias' => 'I18n__' . $field, + 'table' => $joinTable, + 'conditions' => array( + $model->alias . '.' . $model->primaryKey => $db->identifier("I18n__{$field}.foreign_key"), + 'I18n__' . $field . '.model' => $model->name, + 'I18n__' . $field . '.' . $RuntimeModel->displayField => $aliasField, + 'I18n__' . $field . '.locale' => $locale + ) + ); + } + return $query; + } + +/** + * afterFind Callback + * + * @param Model $model Model find was run on + * @param array $results Array of model results. + * @param boolean $primary Did the find originate on $model. + * @return array Modified results + */ + public function afterFind(Model $model, $results, $primary) { + $model->virtualFields = $this->runtime[$model->alias]['virtualFields']; + $this->runtime[$model->alias]['virtualFields'] = $this->runtime[$model->alias]['fields'] = array(); + $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; + + if (is_array($locale)) { + foreach ($locale as $_locale) { + if (!isset($row[$model->alias][$aliasField]) && !empty($row[$model->alias]['i18n_' . $field . '_' . $_locale])) { + $row[$model->alias][$aliasField] = $row[$model->alias]['i18n_' . $field . '_' . $_locale]; + $row[$model->alias]['locale'] = $_locale; + } + unset($row[$model->alias]['i18n_' . $field . '_' . $_locale]); + } + + if (!isset($row[$model->alias][$aliasField])) { + $row[$model->alias][$aliasField] = ''; + } + } else { + $value = ''; + if (!empty($row[$model->alias]['i18n_' . $field])) { + $value = $row[$model->alias]['i18n_' . $field]; + } + $row[$model->alias][$aliasField] = $value; + unset($row[$model->alias]['i18n_' . $field]); + } + } + } + return $results; + } + +/** + * beforeValidate Callback + * + * @param Model $model Model invalidFields was called on. + * @return boolean + */ + public function beforeValidate(Model $model) { + 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. + * @return boolean true. + */ + public function beforeSave(Model $model, $options = array()) { + if (isset($options['validate']) && $options['validate'] == false) { + 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 + * @return void + */ + 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; + } + +/** + * afterSave Callback + * + * @param Model $model Model the callback is called on + * @param boolean $created Whether or not the save created a record. + * @return void + */ + public function afterSave(Model $model, $created) { + if (!isset($this->runtime[$model->alias]['beforeValidate']) && !isset($this->runtime[$model->alias]['beforeSave'])) { + return true; + } + $locale = $this->_getLocale($model); + 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->alias, 'foreign_key' => $model->id); + $RuntimeModel = $this->translateModel($model); + + $fields = array_merge($this->settings[$model->alias], $this->runtime[$model->alias]['fields']); + if ($created) { + foreach ($fields as $field) { + if (!isset($tempData[$field])) { + //set the field value to an empty string + $tempData[$field] = ''; + } + } + } + + 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])))); + } else { + $RuntimeModel->save(array($RuntimeModel->alias => $conditions)); + } + } + } + } + +/** + * 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->alias, '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) || is_null($model->locale)) { + $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, 'Model'); + } + 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 instance of model + * @param string|array $fields string with field or array(field1, field2=>AssocName, field3) + * @param boolean $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 boolean + * @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'); + + 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 (is_null($association)) { + if ($reset) { + $this->runtime[$model->alias]['fields'][] = $field; + } else { + $this->settings[$model->alias][] = $field; + } + } else { + if ($reset) { + $this->runtime[$model->alias]['fields'][$field] = $association; + } 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->alias, + $RuntimeModel->displayField => $field + ))); + } + } + + if (!empty($associations)) { + $model->bindModel(array('hasMany' => $associations), $reset); + } + return true; + } + +/** + * Update runtime setting for a given field. + * + * @param string $field The field to update. + */ + 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 instance of model + * @param string|array $fields string with field, or array(field1, field2=>AssocName, field3), or null for + * unbind all original translations + * @return boolean + */ + 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); + } + $RuntimeModel = $this->translateModel($model); + $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 (!is_null($association) && (isset($model->hasMany[$association]) || isset($model->__backAssociation['hasMany'][$association]))) { + $associations[] = $association; + } + } + + if (!empty($associations)) { + $model->unbindModel(array('hasMany' => $associations), false); + } + return true; + } + +} + diff --git a/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Behavior/TreeBehavior.php b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Behavior/TreeBehavior.php new file mode 100644 index 0000000..a92c367 --- /dev/null +++ b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Behavior/TreeBehavior.php @@ -0,0 +1,1007 @@ + 'parent_id', 'left' => 'lft', 'right' => 'rght', + 'scope' => '1 = 1', 'type' => 'nested', '__parentChange' => false, 'recursive' => -1 + ); + +/** + * Used to preserve state between delete callbacks. + * + * @var array + */ + protected $_deletedRow = null; + +/** + * Initiate Tree behavior + * + * @param Model $Model instance 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 = array_merge($this->_defaults, $config); + + if (in_array($settings['scope'], $Model->getAssociated('belongsTo'))) { + $data = $Model->getAssociated($settings['scope']); + $parent = $Model->{$settings['scope']}; + $settings['scope'] = $Model->alias . '.' . $data['foreignKey'] . ' = ' . $parent->alias . '.' . $parent->primaryKey; + $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 instance. + * @param boolean $created indicates whether the node just saved was created or updated + * @return boolean true on success, false on failure + */ + public function afterSave(Model $Model, $created) { + 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; + return $this->_setParent($Model, $Model->data[$Model->alias][$parent]); + } + } + +/** + * 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 instance + * @param boolean $cascade + * @return boolean + */ + public function beforeDelete(Model $Model, $cascade = true) { + extract($this->settings[$Model->alias]); + $data = $Model->find('first', array( + 'conditions' => array($Model->alias . '.' . $Model->primaryKey => $Model->id), + 'fields' => array($Model->alias . '.' . $left, $Model->alias . '.' . $right), + 'recursive' => -1)); + if ($data) { + $this->_deletedRow = 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 instance + * @return boolean true to continue, false to abort the delete + */ + public function afterDelete(Model $Model) { + extract($this->settings[$Model->alias]); + $data = $this->_deletedRow; + $this->_deletedRow = 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->alias}.{$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. + * + * @since 1.2 + * @param Model $Model Model instance + * @return boolean true to continue, false to abort the save + */ + public function beforeSave(Model $Model) { + extract($this->settings[$Model->alias]); + + $this->_addToWhitelist($Model, array($left, $right)); + if (!$Model->id) { + if (array_key_exists($parent, $Model->data[$Model->alias]) && $Model->data[$Model->alias][$parent]) { + $parentNode = $Model->find('first', array( + 'conditions' => array($scope, $Model->escapeField() => $Model->data[$Model->alias][$parent]), + 'fields' => array($Model->primaryKey, $right), 'recursive' => $recursive + )); + if (!$parentNode) { + return false; + } + list($parentNode) = array_values($parentNode); + $Model->data[$Model->alias][$left] = 0; + $Model->data[$Model->alias][$right] = 0; + } else { + $edge = $this->_getMax($Model, $scope, $right, $recursive); + $Model->data[$Model->alias][$left] = $edge + 1; + $Model->data[$Model->alias][$right] = $edge + 2; + } + } elseif (array_key_exists($parent, $Model->data[$Model->alias])) { + 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); + } else { + $values = $Model->find('first', array( + 'conditions' => array($scope, $Model->escapeField() => $Model->id), + 'fields' => array($Model->primaryKey, $parent, $left, $right), 'recursive' => $recursive) + ); + + if ($values === false) { + return false; + } + list($node) = array_values($values); + + $parentNode = $Model->find('first', array( + 'conditions' => array($scope, $Model->escapeField() => $Model->data[$Model->alias][$parent]), + 'fields' => array($Model->primaryKey, $left, $right), 'recursive' => $recursive + )); + if (!$parentNode) { + return false; + } + list($parentNode) = array_values($parentNode); + + if (($node[$left] < $parentNode[$left]) && ($parentNode[$right] < $node[$right])) { + return false; + } elseif ($node[$Model->primaryKey] == $parentNode[$Model->primaryKey]) { + return false; + } + } + } + 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 instance + * @param integer|string|boolean $id The ID of the record to read or false to read all top level nodes + * @param boolean $direct whether to count direct, or all, children + * @return integer number of child nodes + * @link http://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 = $Model->find('first', array('conditions' => array($scope, $Model->escapeField() => $id), 'recursive' => $recursive)); + 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 instance + * @param integer|string $id The ID of the record to read + * @param boolean $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 integer $limit SQL LIMIT clause, for calculating items per page. + * @param integer $page Page number, for accessing paged data + * @param integer $recursive The number of levels deep to fetch associated records + * @return array Array of child nodes + * @link http://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) { + if (is_array($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 (!is_null($overrideRecursive)) { + $recursive = $overrideRecursive; + } + if (!$order) { + $order = $Model->alias . '.' . $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 + ))); + + if (empty($result) || !isset($result[0])) { + return array(); + } + $conditions = array($scope, + $Model->escapeField($right) . ' <' => $result[0][$right], + $Model->escapeField($left) . ' >' => $result[0][$left] + ); + } + return $Model->find('all', compact('conditions', 'fields', 'order', 'limit', 'page', 'recursive')); + } + +/** + * A convenience method for returning a hierarchical array used for HTML select boxes + * + * @param Model $Model Model instance + * @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 integer $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 http://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 (!is_null($overrideRecursive)) { + $recursive = $overrideRecursive; + } + + if ($keyPath == null && $valuePath == null && $Model->hasField($Model->displayField)) { + $fields = array($Model->primaryKey, $Model->displayField, $left, $right); + } else { + $fields = null; + } + + if ($keyPath == null) { + $keyPath = '{n}.' . $Model->alias . '.' . $Model->primaryKey; + } + + if ($valuePath == null) { + $valuePath = array('%s%s', '{n}.tree_prefix', '{n}.' . $Model->alias . '.' . $Model->displayField); + + } elseif (is_string($valuePath)) { + $valuePath = array('%s%s', '{n}.tree_prefix', $valuePath); + + } else { + $valuePath[0] = '{' . (count($valuePath) - 1) . '}' . $valuePath[0]; + $valuePath[] = '{n}.tree_prefix'; + } + $order = $Model->alias . '.' . $left . ' asc'; + $results = $Model->find('all', compact('conditions', 'fields', 'order', 'recursive')); + $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($spacer, $count); + $stack[] = $result[$Model->alias][$right]; + } + if (empty($results)) { + return array(); + } + return Hash::combine($results, $keyPath, $valuePath); + } + +/** + * Get the parent node + * + * reads the parent id and returns this node + * + * @param Model $Model Model instance + * @param integer|string $id The ID of the record to read + * @param string|array $fields + * @param integer $recursive The number of levels deep to fetch associated records + * @return array|boolean Array of data for the parent node + * @link http://book.cakephp.org/2.0/en/core-libraries/behaviors/tree.html#TreeBehavior::getParentNode + */ + public function getParentNode(Model $Model, $id = null, $fields = null, $recursive = null) { + if (is_array($id)) { + extract(array_merge(array('id' => null), $id)); + } + $overrideRecursive = $recursive; + if (empty ($id)) { + $id = $Model->id; + } + extract($this->settings[$Model->alias]); + if (!is_null($overrideRecursive)) { + $recursive = $overrideRecursive; + } + $parentId = $Model->find('first', array('conditions' => array($Model->primaryKey => $id), 'fields' => array($parent), 'recursive' => -1)); + + if ($parentId) { + $parentId = $parentId[$Model->alias][$parent]; + $parent = $Model->find('first', array('conditions' => array($Model->escapeField() => $parentId), 'fields' => $fields, 'recursive' => $recursive)); + + return $parent; + } + return false; + } + +/** + * Get the path to the given node + * + * @param Model $Model Model instance + * @param integer|string $id The ID of the record to read + * @param string|array $fields Either a single string of a field name, or an array of field names + * @param integer $recursive The number of levels deep to fetch associated records + * @return array Array of nodes from top most parent to current node + * @link http://book.cakephp.org/2.0/en/core-libraries/behaviors/tree.html#TreeBehavior::getPath + */ + public function getPath(Model $Model, $id = null, $fields = null, $recursive = null) { + if (is_array($id)) { + extract(array_merge(array('id' => null), $id)); + } + $overrideRecursive = $recursive; + if (empty ($id)) { + $id = $Model->id; + } + extract($this->settings[$Model->alias]); + if (!is_null($overrideRecursive)) { + $recursive = $overrideRecursive; + } + $result = $Model->find('first', array('conditions' => array($Model->escapeField() => $id), 'fields' => array($left, $right), 'recursive' => $recursive)); + if ($result) { + $result = array_values($result); + } else { + return null; + } + $item = $result[0]; + $results = $Model->find('all', array( + 'conditions' => array($scope, $Model->escapeField($left) . ' <=' => $item[$left], $Model->escapeField($right) . ' >=' => $item[$right]), + 'fields' => $fields, 'order' => array($Model->escapeField($left) => 'asc'), 'recursive' => $recursive + )); + 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 instance + * @param integer|string $id The ID of the record to move + * @param integer|boolean $number how many places to move the node or true to move to last position + * @return boolean true on success, false on failure + * @link http://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($Model->find('first', array( + 'conditions' => array($scope, $Model->escapeField() => $id), + 'fields' => array($Model->primaryKey, $left, $right, $parent), 'recursive' => $recursive + ))); + if ($node[$parent]) { + list($parentNode) = array_values($Model->find('first', array( + 'conditions' => array($scope, $Model->escapeField() => $node[$parent]), + 'fields' => array($Model->primaryKey, $left, $right), 'recursive' => $recursive + ))); + 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), '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 instance + * @param integer|string $id The ID of the record to move + * @param integer|boolean $number how many places to move the node, or true to move to first position + * @return boolean true on success, false on failure + * @link http://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($Model->find('first', array( + 'conditions' => array($scope, $Model->escapeField() => $id), + 'fields' => array($Model->primaryKey, $left, $right, $parent), 'recursive' => $recursive + ))); + if ($node[$parent]) { + list($parentNode) = array_values($Model->find('first', array( + 'conditions' => array($scope, $Model->escapeField() => $node[$parent]), + 'fields' => array($Model->primaryKey, $left, $right), 'recursive' => $recursive + ))); + 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), + '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. + * + * @todo Could be written to be faster, *maybe*. Ideally using a subquery and putting all the logic burden on the DB. + * @param Model $Model Model instance + * @param string $mode parent or tree + * @param string|integer $missingParentAction 'return' to do nothing and return, 'delete' to + * delete, or the id of the parent to set as the parent_id + * @return boolean true on success, false on failure + * @link http://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 + )) + )); + $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->primaryKey => array_flip($missingParents))); + } else { + $Model->updateAll(array($parent => $missingParentAction), array($Model->escapeField($Model->primaryKey) => array_flip($missingParents))); + } + } + $count = 1; + foreach ($Model->find('all', array('conditions' => $scope, 'fields' => array($Model->primaryKey), 'order' => $left)) as $array) { + $lft = $count++; + $rght = $count++; + $Model->create(false); + $Model->id = $array[$Model->alias][$Model->primaryKey]; + $Model->save(array($left => $lft, $right => $rght), array('callbacks' => false, 'validate' => false)); + } + foreach ($Model->find('all', array('conditions' => $scope, 'fields' => array($Model->primaryKey, $parent), 'order' => $left)) as $array) { + $Model->create(false); + $Model->id = $array[$Model->alias][$Model->primaryKey]; + $this->_setParent($Model, $array[$Model->alias][$parent]); + } + } 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]); + if ($path == null || count($path) < 2) { + $parentId = null; + } else { + $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; + } + +/** + * 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 instance + * @param array $options array of options to use in reordering. + * @return boolean true on success, false on failure + * @link http://book.cakephp.org/2.0/en/core-libraries/behaviors/tree.html#TreeBehavior::reorder + */ + public function reorder(Model $Model, $options = array()) { + $options = array_merge(array('id' => null, 'field' => $Model->displayField, 'order' => 'ASC', 'verify' => true), $options); + 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 instance + * @param integer|string $id The ID of the record to remove + * @param boolean $delete whether to delete the node after reparenting children (if any) + * @return boolean true on success, false on failure + * @link http://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($Model->find('first', array( + 'conditions' => array($scope, $Model->escapeField() => $id), + 'fields' => array($Model->primaryKey, $left, $right, $parent), + 'recursive' => $recursive + ))); + + if ($node[$right] == $node[$left] + 1) { + if ($delete) { + return $Model->delete($id); + } else { + $Model->id = $id; + return $Model->saveField($parent, null); + } + } elseif ($node[$parent]) { + list($parentNode) = array_values($Model->find('first', array( + 'conditions' => array($scope, $Model->escapeField() => $node[$parent]), + 'fields' => array($Model->primaryKey, $left, $right), + 'recursive' => $recursive + ))); + } 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); + } else { + $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 instance + * @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 http://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 == 0) { + $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)), '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) + )))); + + foreach ($Model->find('all', array('conditions' => $scope, 'recursive' => 0)) as $instance) { + if (is_null($instance[$Model->alias][$left]) || is_null($instance[$Model->alias][$right])) { + $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; + } + +/** + * 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 instance + * @param integer|string $parentId + * @param boolean $created + * @return boolean true on success, false on failure + */ + protected function _setParent(Model $Model, $parentId = null, $created = false) { + extract($this->settings[$Model->alias]); + list($node) = array_values($Model->find('first', array( + 'conditions' => array($scope, $Model->escapeField() => $Model->id), + 'fields' => array($Model->primaryKey, $parent, $left, $right), + 'recursive' => $recursive + ))); + $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 = $Model->find('first', array( + 'conditions' => array($scope, $Model->escapeField() => $parentId), + 'fields' => array($Model->primaryKey, $left, $right), + 'recursive' => $recursive + )); + + 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 + * @param string $scope + * @param string $right + * @param integer $recursive + * @param boolean $created + * @return integer + */ + 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->alias}.{$Model->primaryKey} <> "; + $scope .= $db->value($Model->id, $Model->getColumnType($Model->primaryKey)); + } else { + $scope['NOT'][$Model->alias . '.' . $Model->primaryKey] = $Model->id; + } + } + $name = $Model->alias . '.' . $right; + list($edge) = array_values($Model->find('first', array( + 'conditions' => $scope, + 'fields' => $db->calculate($Model, 'max', array($name, $right)), + 'recursive' => $recursive + ))); + return (empty($edge[$right])) ? 0 : $edge[$right]; + } + +/** + * get the minimum index value in the table. + * + * @param Model $Model + * @param string $scope + * @param string $left + * @param integer $recursive + * @return integer + */ + protected function _getMin(Model $Model, $scope, $left, $recursive = -1) { + $db = ConnectionManager::getDataSource($Model->useDbConfig); + $name = $Model->alias . '.' . $left; + list($edge) = array_values($Model->find('first', array( + 'conditions' => $scope, + 'fields' => $db->calculate($Model, 'min', array($name, $left)), + 'recursive' => $recursive + ))); + return (empty($edge[$left])) ? 0 : $edge[$left]; + } + +/** + * Table sync method. + * + * Handles table sync operations, Taking account of the behavior scope. + * + * @param Model $Model + * @param integer $shift + * @param string $dir + * @param array $conditions + * @param boolean $created + * @param string $field + * @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->alias}.{$field} {$conditions}"); + } + if (($scope != '1 = 1' && $scope !== true) && $scope) { + $conditions[] = $scope; + } + if ($created) { + $conditions['NOT'][$Model->alias . '.' . $Model->primaryKey] = $Model->id; + } + $Model->updateAll(array($Model->alias . '.' . $field => $Model->escapeField($field) . ' ' . $dir . ' ' . $shift), $conditions); + $Model->recursive = $ModelRecursive; + } + +} diff --git a/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/BehaviorCollection.php b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/BehaviorCollection.php new file mode 100644 index 0000000..fff3e7d --- /dev/null +++ b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/BehaviorCollection.php @@ -0,0 +1,296 @@ +modelName = $modelName; + + if (!empty($behaviors)) { + foreach (BehaviorCollection::normalizeObjectArray($behaviors) as $behavior => $config) { + $this->load($config['class'], $config['settings']); + } + } + } + +/** + * Backwards compatible alias for load() + * + * @param string $behavior + * @param array $config + * @return void + * @deprecated 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 boolean True on success, false on failure + * @throws MissingBehaviorException when a behavior could not be found. + */ + public function load($behavior, $config = array()) { + if (is_array($config) && isset($config['className'])) { + $alias = $behavior; + $behavior = $config['className']; + } + $configDisabled = isset($config['enabled']) && $config['enabled'] === false; + unset($config['enabled'], $config['className']); + + list($plugin, $name) = pluginSplit($behavior, true); + if (!isset($alias)) { + $alias = $name; + } + + $class = $name . 'Behavior'; + + 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]); + if (!empty($plugin)) { + ClassRegistry::addObject($plugin . '.' . $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]->setup(ClassRegistry::getObject($this->modelName), $config); + + 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] = array($alias, $m); + } + } + } + + if (!in_array($alias, $this->_enabled) && !$configDisabled) { + $this->enable($alias); + } else { + $this->disable($alias); + } + return true; + } + +/** + * Detaches a behavior from a model + * + * @param string $name CamelCased name of the behavior to unload + * @return void + */ + public function unload($name) { + list($plugin, $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 Use unload instead. + */ + public function detach($name) { + return $this->unload($name); + } + +/** + * 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 boolean $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); + + if ($strict && empty($method)) { + trigger_error(__d('cake_dev', "BehaviorCollection::dispatchMethod() - Method %s not found in any attached behavior", $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) + ); + } + +/** + * 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 boolean $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; + } + +/** + * 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' + ); + } + +} diff --git a/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/CakeSchema.php b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/CakeSchema.php new file mode 100644 index 0000000..60c3b76 --- /dev/null +++ b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/CakeSchema.php @@ -0,0 +1,710 @@ +name = preg_replace('/schema$/i', '', get_class($this)); + } + if (!empty($options['plugin'])) { + $this->plugin = $options['plugin']; + } + + if (strtolower($this->name) === 'cake') { + $this->name = Inflector::camelize(Inflector::slug(Configure::read('App.dir'))); + } + + if (empty($options['path'])) { + $this->path = APP . 'Config' . DS . '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 boolean 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 Set of name and tables + */ + public function load($options = array()) { + if (is_string($options)) { + $options = array('path' => $options); + } + + $this->build($options); + extract(get_object_vars($this)); + + $class = $name . 'Schema'; + + if (!class_exists($class)) { + if (file_exists($path . DS . $file) && is_file($path . DS . $file)) { + require_once $path . DS . $file; + } elseif (file_exists($path . DS . 'schema.php') && is_file($path . DS . 'schema.php')) { + require_once $path . DS . 'schema.php'; + } + } + + 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()) { + extract(array_merge( + array( + 'connection' => $this->connection, + 'name' => $this->name, + 'models' => true, + ), + $options + )); + $db = ConnectionManager::getDataSource($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($models) && $models !== false) { + if (isset($this->plugin)) { + $models = App::objects($this->plugin . '.Model', null, false); + } else { + $models = App::objects('Model'); + } + } + + if (is_array($models)) { + foreach ($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'] != $connection) { + continue; + } + + try { + $Object = ClassRegistry::init(array('class' => $model, 'ds' => $connection)); + } catch (CakeException $e) { + continue; + } + + $db = $Object->getDataSource(); + if (is_object($Object) && $Object->useTable !== false) { + $fulltable = $table = $db->fullTableName($Object, false, false); + if ($prefix && strpos($table, $prefix) !== 0) { + continue; + } + $table = $this->_noPrefixTable($prefix, $table); + + if (in_array($fulltable, $currentTables)) { + $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)) { + foreach ($Object->hasAndBelongsToMany as $Assoc => $assocData) { + if (isset($assocData['with'])) { + $class = $assocData['with']; + } + if (is_object($Object->$class)) { + $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' => $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 ($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 compact('name', '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); + } + + extract(array_merge( + get_object_vars($this), $options + )); + + $out = "class {$name}Schema extends CakeSchema {\n\n"; + + if ($path !== $this->path) { + $out .= "\tpublic \$path = '{$path}';\n\n"; + } + + if ($file !== $this->file) { + $out .= "\tpublic \$file = '{$file}';\n\n"; + } + + if ($connection !== 'default') { + $out .= "\tpublic \$connection = '{$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($tables)) { + $this->read(); + } + + foreach ($tables as $table => $fields) { + if (!is_numeric($table) && $table !== 'missing') { + $out .= $this->generateTable($table, $fields); + } + } + $out .= "}\n"; + + $file = new File($path . DS . $file, true); + $content = "write($content)) { + return $content; + } + return false; + } + +/** + * Generate the code for a table. Takes a table name and $fields array + * Returns a completed 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 + */ + public function generateTable($table, $fields) { + $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); + } + $col = "\t\t'{$field}' => array('type' => '" . $value['type'] . "', "; + unset($value['type']); + $col .= join(', ', $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(" . join(', ', $this->_values($index)) . ")"; + } + $col .= join(",\n\t\t\t", $props) . "\n\t\t"; + } elseif ($field == 'tableParameters') { + $col = "\t\t'tableParameters' => array("; + $props = array(); + foreach ((array)$value as $key => $param) { + $props[] = "'{$key}' => '$param'"; + } + $col .= join(', ', $props); + } + $col .= ")"; + $cols[] = $col; + } + $out .= join(",\n", $cols); + } + $out .= "\n\t);\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]['add'] = $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) && $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 (is_null($value) !== is_null($correspondingValue)) { + $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("', '", $val) . "')"; + } elseif (!is_numeric($key)) { + $val = var_export($val, true); + if ($val === 'NULL') { + $val = 'null'; + } + $vals[] = "'{$key}' => {$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); + + $columns = $props = array(); + foreach ($fields as $name => $value) { + if ($Obj->primaryKey == $name) { + $value['key'] = 'primary'; + } + 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)) { + 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); + } + +} diff --git a/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/ConnectionManager.php b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/ConnectionManager.php new file mode 100644 index 0000000..edbcfa5 --- /dev/null +++ b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/ConnectionManager.php @@ -0,0 +1,266 @@ +{$name}); + self::$_dataSources[$name]->configKeyName = $name; + + return self::$_dataSources[$name]; + } + +/** + * 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(self::$_init)) { + self::_init(); + } + return array_keys(self::$_dataSources); + } + +/** + * Gets a DataSource name from an object reference. + * + * @param DataSource $source DataSource object + * @return string Datasource name, or null if source is not present + * in the ConnectionManager. + */ + public static function getSourceName($source) { + if (empty(self::$_init)) { + self::_init(); + } + foreach (self::$_dataSources as $name => $ds) { + if ($ds === $source) { + return $name; + } + } + return null; + } + +/** + * 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 boolean True on success, null on failure or false if the class is already loaded + * @throws MissingDatasourceException + */ + public static function loadDataSource($connName) { + if (empty(self::$_init)) { + self::_init(); + } + + if (is_array($connName)) { + $conn = $connName; + } else { + $conn = self::$_connectionsEnum[$connName]; + } + + if (class_exists($conn['classname'], false)) { + return false; + } + + $plugin = $package = null; + if (!empty($conn['plugin'])) { + $plugin = $conn['plugin'] . '.'; + } + if (!empty($conn['package'])) { + $package = '/' . $conn['package']; + } + + 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 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(self::$_init)) { + self::_init(); + } + return (array)self::$config; + } + +/** + * 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 A reference to the DataSource object, or null if creation failed + */ + public static function create($name = '', $config = array()) { + if (empty(self::$_init)) { + self::_init(); + } + + if (empty($name) || empty($config) || array_key_exists($name, self::$_connectionsEnum)) { + return null; + } + self::$config->{$name} = $config; + self::$_connectionsEnum[$name] = self::_connectionData($config); + $return = self::getDataSource($name); + return $return; + } + +/** + * Removes a connection configuration at runtime given its name + * + * @param string $name the connection name as it was created + * @return boolean success if connection was removed, false if it does not exist + */ + public static function drop($name) { + if (empty(self::$_init)) { + self::_init(); + } + + if (!isset(self::$config->{$name})) { + return false; + } + unset(self::$_connectionsEnum[$name], self::$_dataSources[$name], self::$config->{$name}); + return true; + } + +/** + * 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(self::$config->{$name})) { + self::$_connectionsEnum[$name] = self::_connectionData(self::$config->{$name}); + } else { + throw new MissingDatasourceConfigException(array('config' => $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; + + list($plugin, $classname) = pluginSplit($config['datasource']); + if (strpos($classname, '/') !== false) { + $package = dirname($classname); + $classname = basename($classname); + } + return compact('package', 'classname', 'plugin'); + } + +} diff --git a/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Datasource/CakeSession.php b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Datasource/CakeSession.php new file mode 100644 index 0000000..6baab1d --- /dev/null +++ b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Datasource/CakeSession.php @@ -0,0 +1,688 @@ + 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 integer $errorNumber Error to set + * @return string Error as string + */ + protected static function _error($errorNumber) { + if (!is_array(self::$error) || !array_key_exists($errorNumber, self::$error)) { + return false; + } else { + return self::$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 (self::$lastError) { + return self::_error(self::$lastError); + } + return false; + } + +/** + * Returns true if session is valid. + * + * @return boolean Success + */ + public static function valid() { + if (self::read('Config')) { + if (self::_validAgentAndTime() && self::$error === false) { + self::$valid = true; + } else { + self::$valid = false; + self::_setError(1, 'Session Highjacking Attempted !!!'); + } + } + return self::$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 self::$time + * against the time the session is set to expire. The User agent is only checked + * if Session.checkAgent == true. + * + * @return boolean + */ + protected static function _validAgentAndTime() { + $config = self::read('Config'); + $validAgent = ( + Configure::read('Session.checkAgent') === false || + self::$_userAgent == $config['userAgent'] + ); + return ($validAgent && self::$time <= $config['time']); + } + +/** + * Get / Set the userAgent + * + * @param string $userAgent Set the userAgent + * @return void + */ + public static function userAgent($userAgent = null) { + if ($userAgent) { + self::$_userAgent = $userAgent; + } + if (empty(self::$_userAgent)) { + CakeSession::init(self::$path); + } + return self::$_userAgent; + } + +/** + * Returns given session variable, or all of them, if no parameters given. + * + * @param string|array $name The name of the session variable (or a path as sent to Set.extract) + * @return mixed The value of the session variable + */ + public static function read($name = null) { + if (!self::started() && !self::start()) { + return false; + } + if (is_null($name)) { + return self::_returnSessionVars(); + } + if (empty($name)) { + return false; + } + $result = Hash::get($_SESSION, $name); + + if (isset($result)) { + return $result; + } + self::_setError(2, "$name doesn't exist"); + return null; + } + +/** + * Returns all session variables. + * + * @return mixed Full $_SESSION array, or false on error. + */ + protected static function _returnSessionVars() { + if (!empty($_SESSION)) { + return $_SESSION; + } + self::_setError(2, 'No Session vars set'); + return false; + } + +/** + * Writes value to given session variable name. + * + * @param string|array $name Name of variable + * @param string $value Value to write + * @return boolean True if the write was successful, false if the write failed + */ + public static function write($name, $value = null) { + if (!self::started() && !self::start()) { + return false; + } + if (empty($name)) { + return false; + } + $write = $name; + if (!is_array($name)) { + $write = array($name => $value); + } + foreach ($write as $key => $val) { + self::_overwrite($_SESSION, Hash::insert($_SESSION, $key, $val)); + if (Hash::get($_SESSION, $key) !== $val) { + return false; + } + } + return true; + } + +/** + * Helper method to destroy invalid sessions. + * + * @return void + */ + public static function destroy() { + if (self::started()) { + session_destroy(); + } + self::clear(); + } + +/** + * Clears the session, the session id, and renew's the session. + * + * @return void + */ + public static function clear() { + $_SESSION = null; + self::$id = null; + self::start(); + self::renew(); + } + +/** + * Helper method to initialize a session, based on Cake 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'); + $iniSet = function_exists('ini_set'); + + if (isset($sessionConfig['defaults'])) { + $defaults = self::_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['ini']['session.cookie_lifetime'])) { + $sessionConfig['ini']['session.cookie_lifetime'] = $sessionConfig['cookieTimeout'] * 60; + } + if (!isset($sessionConfig['ini']['session.name'])) { + $sessionConfig['ini']['session.name'] = $sessionConfig['cookie']; + } + if (!empty($sessionConfig['handler'])) { + $sessionConfig['ini']['session.save_handler'] = 'user'; + } + 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; + } + + if (empty($_SESSION)) { + if (!empty($sessionConfig['ini']) && is_array($sessionConfig['ini'])) { + foreach ($sessionConfig['ini'] as $setting => $value) { + if (ini_set($setting, $value) === false) { + throw new CakeSessionException(sprintf( + __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'])) { + $handler = self::_getHandler($sessionConfig['handler']['engine']); + 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); + self::$sessionTime = self::$time + ($sessionConfig['timeout'] * 60); + } + +/** + * Find the handler class and make sure it implements the correct interface. + * + * @param string $handler + * @return void + * @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 + * @return boolean|array + */ + protected static function _defaultConfig($name) { + $defaults = array( + 'php' => array( + 'cookie' => 'CAKEPHP', + 'timeout' => 240, + 'ini' => array( + 'session.use_trans_sid' => 0, + 'session.cookie_path' => self::$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' => self::$path, + 'session.auto_start' => 0, + '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.auto_start' => 0, + 'session.use_cookies' => 1, + 'session.cookie_path' => self::$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.auto_start' => 0, + 'session.use_cookies' => 1, + 'session.cookie_path' => self::$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 boolean Success + */ + protected static function _startSession() { + if (headers_sent()) { + if (empty($_SESSION)) { + $_SESSION = array(); + } + } else { + // For IE<=8 + session_cache_limiter("must-revalidate"); + session_start(); + } + return true; + } + +/** + * Helper method to create a new session. + * + * @return void + */ + protected static function _checkValid() { + if (!self::started() && !self::start()) { + self::$valid = false; + return false; + } + if ($config = self::read('Config')) { + $sessionConfig = Configure::read('Session'); + + if (self::_validAgentAndTime()) { + self::write('Config.time', self::$sessionTime); + if (isset($sessionConfig['autoRegenerate']) && $sessionConfig['autoRegenerate'] === true) { + $check = $config['countdown']; + $check -= 1; + self::write('Config.countdown', $check); + + if ($check < 1) { + self::renew(); + self::write('Config.countdown', self::$requestCountdown); + } + } + self::$valid = true; + } else { + self::destroy(); + self::$valid = false; + self::_setError(1, 'Session Highjacking Attempted !!!'); + } + } else { + self::write('Config.userAgent', self::$_userAgent); + self::write('Config.time', self::$sessionTime); + self::write('Config.countdown', self::$requestCountdown); + self::$valid = true; + } + } + +/** + * Restarts this session. + * + * @return void + */ + public static function renew() { + if (session_id()) { + if (session_id() != '' || isset($_COOKIE[session_name()])) { + setcookie(Configure::read('Session.cookie'), '', time() - 42000, self::$path); + } + session_regenerate_id(true); + } + } + +/** + * Helper method to set an internal error message. + * + * @param integer $errorNumber Number of the error + * @param string $errorMessage Description of the error + * @return void + */ + protected static function _setError($errorNumber, $errorMessage) { + if (self::$error === false) { + self::$error = array(); + } + self::$error[$errorNumber] = $errorMessage; + self::$lastError = $errorNumber; + } + +} diff --git a/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Datasource/DataSource.php b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Datasource/DataSource.php new file mode 100644 index 0000000..e5e665f --- /dev/null +++ b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Datasource/DataSource.php @@ -0,0 +1,442 @@ +setConfig($config); + } + +/** + * Caches/returns cached results for child instances + * + * @param mixed $data + * @return array 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 + * @return array 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 boolean Returns true if a transaction is not in progress + */ + public function begin() { + return !$this->_transactionStarted; + } + +/** + * Commit a transaction + * + * @return boolean Returns true if a transaction is in progress + */ + public function commit() { + return $this->_transactionStarted; + } + +/** + * Rollback a transaction + * + * @return boolean 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 boolean 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 + * @return mixed + */ + public function read(Model $model, $queryData = array()) { + 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. + * @return boolean Success + */ + public function update(Model $model, $fields = null, $values = 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 void + */ + public function delete(Model $model, $id = null) { + return false; + } + +/** + * Returns the ID generated from the previous INSERT operation. + * + * @param mixed $source + * @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 + * @return integer 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 + * @return integer 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 boolean 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 array $assocData + * @param Model $model Instance of the model to replace $__cakeID__$ + * @param Model $linkModel Instance of model to replace $__cakeForeignKey__$ + * @param array $stack + * @return string String of query data with placeholders replaced. + * @todo Remove and refactor $assocData, ensure uses of the method have the param removed too. + */ + public function insertQueryData($query, $data, $association, $assocData, Model $model, Model $linkModel, $stack) { + $keys = array('{$__cakeID__$}', '{$__cakeForeignKey__$}'); + + foreach ($keys as $key) { + $val = null; + $type = null; + + if (strpos($query, $key) !== false) { + switch ($key) { + case '{$__cakeID__$}': + if (isset($data[$model->alias]) || isset($data[$association])) { + if (isset($data[$model->alias][$model->primaryKey])) { + $val = $data[$model->alias][$model->primaryKey]; + } elseif (isset($data[$association][$model->primaryKey])) { + $val = $data[$association][$model->primaryKey]; + } + } else { + $found = false; + foreach (array_reverse($stack) as $assoc) { + if (isset($data[$assoc]) && isset($data[$assoc][$model->primaryKey])) { + $val = $data[$assoc][$model->primaryKey]; + $found = true; + break; + } + } + if (!$found) { + $val = ''; + } + } + $type = $model->getColumnType($model->primaryKey); + break; + case '{$__cakeForeignKey__$}': + foreach ($model->associations() as $id => $name) { + foreach ($model->$name as $assocName => $assoc) { + if ($assocName === $association) { + if (isset($assoc['foreignKey'])) { + $foreignKey = $assoc['foreignKey']; + $assocModel = $model->$assocName; + $type = $assocModel->getColumnType($assocModel->primaryKey); + + if (isset($data[$model->alias][$foreignKey])) { + $val = $data[$model->alias][$foreignKey]; + } elseif (isset($data[$association][$foreignKey])) { + $val = $data[$association][$foreignKey]; + } else { + $found = false; + foreach (array_reverse($stack) as $assoc) { + if (isset($data[$assoc]) && isset($data[$assoc][$foreignKey])) { + $val = $data[$assoc][$foreignKey]; + $found = true; + break; + } + } + if (!$found) { + $val = ''; + } + } + } + break 3; + } + } + } + break; + } + if (empty($val) && $val !== '0') { + return false; + } + $query = str_replace($key, $this->value($val, $type), $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 schema name + * @access public + */ + public function getSchemaName() { + return null; + } + +/** + * Closes a connection. Override in subclasses + * + * @return boolean + * @access public + */ + public function close() { + return $this->connected = false; + } + +/** + * Closes the current datasource. + * + */ + public function __destruct() { + if ($this->_transactionStarted) { + $this->rollback(); + } + if ($this->connected) { + $this->close(); + } + } + +} diff --git a/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Datasource/Database/Mysql.php b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Datasource/Database/Mysql.php new file mode 100644 index 0000000..9506a8a --- /dev/null +++ b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Datasource/Database/Mysql.php @@ -0,0 +1,688 @@ + true, + 'host' => 'localhost', + 'login' => 'root', + 'password' => '', + 'database' => 'cake', + 'port' => '3306' + ); + +/** + * Reference to the PDO object connection + * + * @var PDO $_connection + */ + 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 boolean + */ + 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') + ); + +/** + * 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') + ); + +/** + * MySQL column definition + * + * @var array + */ + public $columns = array( + 'primary_key' => array('name' => 'NOT NULL AUTO_INCREMENT'), + 'string' => array('name' => 'varchar', 'limit' => '255'), + 'text' => array('name' => 'text'), + 'integer' => array('name' => 'int', 'limit' => '11', 'formatter' => 'intval'), + 'float' => 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' => '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') + ); + +/** + * Connects to the database using options in the given configuration array. + * + * @return boolean True if the database could be connected, else false + * @throws MissingConnectionException + */ + public function connect() { + $config = $this->config; + $this->connected = false; + try { + $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['unix_socket'])) { + $dsn = "mysql:host={$config['host']};port={$config['port']};dbname={$config['database']}"; + } else { + $dsn = "mysql:unix_socket={$config['unix_socket']};dbname={$config['database']}"; + } + $this->_connection = new PDO( + $dsn, + $config['login'], + $config['password'], + $flags + ); + $this->connected = true; + } catch (PDOException $e) { + throw new MissingConnectionException(array('class' => $e->getMessage())); + } + + $this->_useAlias = (bool)version_compare($this->getVersion(), "4.1", ">="); + + return $this->connected; + } + +/** + * Check whether the MySQL extension is installed/loaded + * + * @return boolean + */ + public function enabled() { + return in_array('mysql', PDO::getAvailableDrivers()); + } + +/** + * Returns an array of sources (tables) in the database. + * + * @param mixed $data + * @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('SHOW TABLES FROM ' . $this->name($this->config['database'])); + + if (!$result) { + $result->closeCursor(); + return array(); + } else { + $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 + * @return void + */ + public function resultSet($results) { + $this->map = array(); + $numFields = $results->columnCount(); + $index = 0; + + while ($numFields-- > 0) { + $column = $results->getColumnMeta($index); + if (empty($column['native_type'])) { + $type = ($column['len'] == 1) ? 'boolean' : 'string'; + } else { + $type = $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 Character set name + */ + public function getCharsetName($name) { + if ((bool)version_compare($this->getVersion(), "5", ">=")) { + $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'])) { + return $cols['CHARACTER_SET_NAME']; + } + } + 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 != null) { + 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 (!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 + * @param array $fields + * @param array $values + * @param mixed $conditions + * @return array + */ + public function update(Model $model, $fields = array(), $values = null, $conditions = null) { + if (!$this->_useAlias) { + return parent::update($model, $fields, $values, $conditions); + } + + if ($values == null) { + $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 + * @param mixed $conditions + * @return boolean 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 = false; + foreach ((array)$conditions as $key => $value) { + if (strpos($key, $model->alias) === false) { + $complexConditions = true; + break; + } + } + 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; + } + +/** + * Sets the database encoding + * + * @param string $enc Database encoding + * @return boolean + */ + 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) { + $indices = $this->_execute('SHOW INDEX FROM ' . $table); + // @codingStandardsIgnoreStart + // MySQL columns don't match the cakephp conventions. + while ($idx = $indices->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; + $index[$idx->Key_name]['unique'] = intval($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; + } + } + // @codingStandardsIgnoreEnd + $indices->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 + * @return array Array 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; + } + $colList[] = 'CHANGE ' . $this->name($field) . ' ' . $this->buildColumn($col); + } + 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 MySQL "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 = ''; + foreach ($schema->tables as $curTable => $columns) { + if (!$table || $table === $curTable) { + $out .= 'DROP TABLE IF EXISTS ' . $this->fullTableName($curTable) . ";\n"; + } + } + return $out; + } + +/** + * 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. + * @todo Implement this method. + */ + protected function _alterTableParameters($table, $parameters) { + if (isset($parameters['change'])) { + return $this->buildTableParameters($parameters['change']); + } + return array(); + } + +/** + * 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 ' . $name; + } + $alter[] = $out; + } + } + if (isset($indexes['add'])) { + foreach ($indexes['add'] as $name => $value) { + $out = 'ADD '; + if ($name == 'PRIMARY') { + $out .= 'PRIMARY '; + $name = null; + } else { + if (!empty($value['unique'])) { + $out .= 'UNIQUE '; + } + } + if (is_array($value['column'])) { + $out .= 'KEY ' . $name . ' (' . implode(', ', array_map(array(&$this, 'name'), $value['column'])) . ')'; + } else { + $out .= 'KEY ' . $name . ' (' . $this->name($value['column']) . ')'; + } + $alter[] = $out; + } + } + return $alter; + } + +/** + * Returns an 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(); + } else { + $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, '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 || strpos($col, 'decimal') !== false) { + return 'float'; + } + if (strpos($col, 'enum') !== false) { + return "enum($vals)"; + } + return 'text'; + } + +/** + * Gets the schema name + * + * @return string The schema name + */ + public function getSchemaName() { + return $this->config['database']; + } + +/** + * Check if the server support nested transactions + * + * @return boolean + */ + public function nestedTransactionSupported() { + return $this->useNestedTransactions && version_compare($this->getVersion(), '4.1', '>='); + } + +} diff --git a/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Datasource/Database/Postgres.php b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Datasource/Database/Postgres.php new file mode 100644 index 0000000..74da346 --- /dev/null +++ b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Datasource/Database/Postgres.php @@ -0,0 +1,907 @@ + true, + 'host' => 'localhost', + 'login' => 'root', + 'password' => '', + 'database' => 'cake', + 'schema' => 'public', + 'port' => 5432, + 'encoding' => '' + ); + +/** + * Columns + * + * @var array + */ + 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'), + 'float' => array('name' => 'float', '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') + ); + +/** + * 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(); + +/** + * Connects to the database using options in the given configuration array. + * + * @return boolean True if successfully connected. + * @throws MissingConnectionException + */ + public function connect() { + $config = $this->config; + $this->connected = false; + try { + $flags = array( + PDO::ATTR_PERSISTENT => $config['persistent'], + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION + ); + $this->_connection = new PDO( + "pgsql:host={$config['host']};port={$config['port']};dbname={$config['database']}", + $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']); + } + } catch (PDOException $e) { + throw new MissingConnectionException(array('class' => $e->getMessage())); + } + + return $this->connected; + } + +/** + * Check if PostgreSQL is enabled/loaded + * + * @return boolean + */ + 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 + * @return array Array of table names in the database + */ + public function listSources($data = null) { + $cache = parent::listSources(); + + if ($cache != null) { + 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(); + } else { + $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; + + if ($fields === null) { + $cols = $this->_execute( + "SELECT DISTINCT table_schema AS schema, column_name AS name, data_type AS type, is_nullable AS null, + column_default AS default, ordinal_position AS position, character_maximum_length AS char_length, + character_octet_length AS oct_length FROM information_schema.columns + WHERE table_name = ? AND table_schema = ? ORDER BY position", + array($table, $this->config['schema']) + ); + + // @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') { + $length = 36; + } else { + $length = intval($c->oct_length); + } + } elseif (!empty($c->char_length)) { + $length = intval($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('/::.*/', '', $c->default) + ), + 'length' => $length + ); + if ($model instanceof Model) { + if ($c->name == $model->primaryKey) { + $fields[$c->name]['key'] = 'primary'; + if ($fields[$c->name]['type'] !== 'string') { + $fields[$c->name]['length'] = 11; + } + } + } + if ( + $fields[$c->name]['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'] == '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 integer + */ + 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]) && isset($this->_sequenceMap[$table][$field])) { + return $this->_sequenceMap[$table][$field]; + } else { + return "{$table}_{$field}_seq"; + } + } + +/** + * 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 boolean $reset true for resetting the sequence, false to leave it as is. + * and if 1, sequences are not modified + * @return boolean 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))) { + $schema = $this->config['schema']; + if (isset($this->_sequenceMap[$table]) && $reset != true) { + foreach ($this->_sequenceMap[$table] as $field => $sequence) { + list($schema, $sequence) = explode('.', $sequence); + $this->_execute("ALTER SEQUENCE \"{$schema}\".\"{$sequence}\" RESTART WITH 1"); + } + } + return true; + } + return false; + } + +/** + * Prepares field names to be quoted by parent + * + * @param string $data + * @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 + * @param string $alias Alias table name + * @param mixed $fields + * @param boolean $quote + * @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 + FROM pg_catalog.pg_class c, pg_catalog.pg_class c2, pg_catalog.pg_index i + WHERE c.oid = ( + SELECT c.oid + FROM pg_catalog.pg_class c LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace + WHERE c.relname ~ '^(" . $table . ")$' + AND pg_catalog.pg_table_is_visible(c.oid) + AND n.nspname ~ '^(" . $this->config['schema'] . ")$' + ) + AND c.oid = i.indrelid AND i.indexrelid = c2.oid + ORDER BY i.indisprimary DESC, i.indisunique DESC, c2.relname", false); + foreach ($indexes as $i => $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': + foreach ($column as $field => $col) { + if (!isset($col['name'])) { + $col['name'] = $field; + } + $fieldName = $this->name($field); + + $default = isset($col['default']) ? $col['default'] : null; + $nullable = isset($col['null']) ? $col['null'] : null; + unset($col['default'], $col['null']); + $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)) { + $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 integer $limit Limit of results returned + * @param integer $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), 'limit') || strpos(strtolower($limit), 'limit') === 0) { + $rt = ' LIMIT'; + } + + $rt .= ' ' . $limit; + if ($offset) { + $rt .= ' OFFSET ' . $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); + $limit = null; + + if (strpos($col, '(') !== false) { + list($col, $limit) = explode('(', $col); + } + + $floats = array( + 'float', 'float4', 'float8', 'double', 'double precision', 'decimal', 'real', 'numeric' + ); + + 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 (strpos($col, 'int') !== false && $col != 'interval'): + return 'integer'; + case (strpos($col, 'char') !== false || $col == 'uuid'): + return 'string'; + case (strpos($col, 'text') !== false): + return 'text'; + case (strpos($col, 'bytea') !== false): + return 'binary'; + case (in_array($col, $floats)): + return 'float'; + default: + return 'text'; + break; + } + } + +/** + * 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 integer An integer representing the length of the column + */ + public function length($real) { + $col = str_replace(array(')', 'unsigned'), '', $real); + $limit = null; + + if (strpos($col, '(') !== false) { + list($col, $limit) = explode('(', $col); + } + if ($col == 'uuid') { + return 36; + } + if ($limit != null) { + return intval($limit); + } + return null; + } + +/** + * resultSet method + * + * @param array $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] = is_null($row[$index]) ? null : $this->boolean($row[$index]); + break; + case 'binary': + case 'bytea': + $resultRow[$table][$column] = is_null($row[$index]) ? null : stream_get_contents($row[$index]); + break; + default: + $resultRow[$table][$column] = $row[$index]; + break; + } + } + return $resultRow; + } else { + $this->_result->closeCursor(); + return false; + } + } + +/** + * Translates between PHP boolean values and PostgreSQL boolean values + * + * @param mixed $data Value to be translated + * @param boolean $quote true to quote a boolean to be used in a query, false to return the boolean value + * @return boolean 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; + break; + } + + if ($quote) { + return ($result) ? 'TRUE' : 'FALSE'; + } + return (bool)$result; + } + +/** + * Sets the database encoding + * + * @param mixed $enc Database encoding + * @return boolean 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 = preg_replace('/integer\([0-9]+\)/', 'integer', parent::buildColumn($column)); + $out = str_replace('integer serial', 'serial', $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 + * @param string $table + * @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; + } + +/** + * Overrides DboSource::renderStatement to handle schema generation with Postgres-style indexes + * + * @param string $type + * @param array $data + * @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}"; + break; + default: + return parent::renderStatement($type, $data); + break; + } + } + +/** + * Gets the schema name + * + * @return string The schema name + */ + public function getSchemaName() { + return $this->config['schema']; + } + +/** + * Check if the server support nested transactions + * + * @return boolean + */ + public function nestedTransactionSupported() { + return $this->useNestedTransactions && version_compare($this->getVersion(), '8.0', '>='); + } + +} diff --git a/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Datasource/Database/Sqlite.php b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Datasource/Database/Sqlite.php new file mode 100644 index 0000000..63ed603 --- /dev/null +++ b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Datasource/Database/Sqlite.php @@ -0,0 +1,571 @@ + false, + 'database' => null + ); + +/** + * SQLite3 column definition + * + * @var array + */ + 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'), + 'float' => 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' => '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 boolean + * @throws MissingConnectionException + */ + public function connect() { + $config = $this->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' => $e->getMessage())); + } + return $this->connected; + } + +/** + * Check whether the SQLite extension is installed/loaded + * + * @return boolean + */ + 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 + * @return array Array of table names in the database + */ + public function listSources($data = null) { + $cache = parent::listSources(); + if ($cache != null) { + return $cache; + } + + $result = $this->fetchAll("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name;", false); + + if (!$result || empty($result)) { + return array(); + } else { + $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 != null) { + return $cache; + } + $fields = array(); + $result = $this->_execute( + 'PRAGMA table_info(' . $this->value($table, 'string') . ')' + ); + + foreach ($result as $column) { + $column = (array)$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 ($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 + * @param array $fields + * @param array $values + * @param mixed $conditions + * @return array + */ + 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 boolean SQL TRUNCATE TABLE statement, false if not applicable. + */ + public function truncate($table) { + $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)); + $limit = null; + @list($col, $limit) = explode('(', $col); + + if (in_array($col, array('text', 'integer', 'float', 'boolean', 'timestamp', 'date', 'datetime', 'time'))) { + return $col; + } + 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 'float'; + } + return 'text'; + } + +/** + * Generate ResultSet + * + * @param mixed $results + * @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) { + $last = strripos($querystring, 'FROM'); + if ($last !== false) { + $selectpart = substr($querystring, 7, $last - 8); + $selects = String::tokenize($selectpart, ',', '(', ')'); + } + } 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\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' && !is_null($row[$col])) { + $resultRow[$table][$column] = $this->boolean($resultRow[$table][$column]); + } + } + return $resultRow; + } else { + $this->_result->closeCursor(); + return false; + } + } + +/** + * Returns a limit statement in the correct format for the particular database. + * + * @param integer $limit Limit of results returned + * @param integer $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), 'limit') || strpos(strtolower($limit), 'limit') === 0) { + $rt = ' LIMIT'; + } + $rt .= ' ' . $limit; + if ($offset) { + $rt .= ' OFFSET ' . $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_merge(array('null' => true), $column); + 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; + } + + if (isset($column['key']) && $column['key'] == 'primary' && $type == 'integer') { + return $this->name($name) . ' ' . $this->columns['primary_key']['name']; + } + return parent::buildColumn($column); + } + +/** + * Sets the database encoding + * + * @param string $enc Database encoding + * @return boolean + */ + 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 + * @param string $table + * @return string + */ + 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'] = join(', ', 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 $i => $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'] = intval($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 + * @param array $data + * @return string + */ + public function renderStatement($type, $data) { + switch (strtolower($type)) { + case 'schema': + extract($data); + if (is_array($columns)) { + $columns = "\t" . join(",\n\t", array_filter($columns)); + } + if (is_array($indexes)) { + $indexes = "\t" . join("\n\t", array_filter($indexes)); + } + return "CREATE TABLE {$table} (\n{$columns});\n{$indexes}"; + break; + default: + return parent::renderStatement($type, $data); + break; + } + } + +/** + * PDO deals in objects, not resources, so overload accordingly. + * + * @return boolean + */ + public function hasResult() { + return is_object($this->_result); + } + +/** + * 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 = ''; + foreach ($schema->tables as $curTable => $columns) { + if (!$table || $table == $curTable) { + $out .= 'DROP TABLE IF EXISTS ' . $this->fullTableName($curTable) . ";\n"; + } + } + return $out; + } + +/** + * 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 boolean + */ + public function nestedTransactionSupported() { + return $this->useNestedTransactions && version_compare($this->getVersion(), '3.6.8', '>='); + } + +} diff --git a/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Datasource/Database/Sqlserver.php b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Datasource/Database/Sqlserver.php new file mode 100644 index 0000000..b23e59a --- /dev/null +++ b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Datasource/Database/Sqlserver.php @@ -0,0 +1,783 @@ + true, + 'host' => 'localhost\SQLEXPRESS', + 'login' => '', + 'password' => '', + 'database' => 'cake', + 'schema' => '', + ); + +/** + * MS SQL column definition + * + * @var array + */ + 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'), + 'float' => array('name' => 'numeric', '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. + */ + const ROW_COUNTER = '_cake_page_rownum_'; + +/** + * Connects to the database using options in the given configuration array. + * + * @return boolean True if the database could be connected, else false + * @throws MissingConnectionException + */ + public function connect() { + $config = $this->config; + $this->connected = false; + try { + $flags = array( + PDO::ATTR_PERSISTENT => $config['persistent'], + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION + ); + if (!empty($config['encoding'])) { + $flags[PDO::SQLSRV_ATTR_ENCODING] = $config['encoding']; + } + $this->_connection = new PDO( + "sqlsrv:server={$config['host']};Database={$config['database']}", + $config['login'], + $config['password'], + $flags + ); + $this->connected = true; + } catch (PDOException $e) { + throw new MissingConnectionException(array('class' => $e->getMessage())); + } + + return $this->connected; + } + +/** + * Check that PDO SQL Server is installed/loaded + * + * @return boolean + */ + public function enabled() { + return in_array('sqlsrv', PDO::getAvailableDrivers()); + } + +/** + * Returns an array of sources (tables) in the database. + * + * @param mixed $data + * @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(); + } else { + $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); + $cache = parent::describe($table); + if ($cache != null) { + return $cache; + } + $fields = array(); + $table = $this->fullTableName($model, false); + $cols = $this->_execute( + "SELECT + COLUMN_NAME as Field, + DATA_TYPE as Type, + COL_LENGTH('" . $table . "', COLUMN_NAME) as Length, + IS_NULLABLE As [Null], + COLUMN_DEFAULT as [Default], + COLUMNPROPERTY(OBJECT_ID('" . $table . "'), COLUMN_NAME, 'IsIdentity') as [Key], + NUMERIC_SCALE as Size + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_NAME = '" . $table . "'" + ); + 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' => preg_replace("/^[(]{1,2}'?([^')]*)?'?[)]{1,2}$/", "$1", $column->Default), + 'length' => $this->length($column), + 'key' => ($column->Key == '1') ? 'primary' : false + ); + + if ($fields[$field]['default'] === 'null') { + $fields[$field]['default'] = null; + } else { + $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 + * @param string $alias Alias table name + * @param array $fields + * @param boolean $quote + * @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) { + $prepend = 'DISTINCT '; + $fields[$i] = trim(str_replace('DISTINCT', '', $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]); + $this->_fieldMappings[$build[0] . '__' . $build[1]] = $fields[$i]; + $fieldName = $this->name($build[0] . '.' . $build[1]); + $fieldAlias = $this->name(preg_replace("/^\[(.+)\]$/", "$1", $build[0]) . '__' . $build[1]); + } + if ($model->getColumnType($fields[$i]) == 'datetime') { + $fieldName = "CONVERT(VARCHAR(20), {$fieldName}, 20)"; + } + $fields[$i] = "{$fieldName} AS {$fieldAlias}"; + } + $result[] = $prepend . $fields[$i]; + } + return $result; + } else { + 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 + * @param array $fields + * @param array $values + * @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 + * @param array $fields + * @param array $values + * @param mixed $conditions + * @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 integer $limit Limit of results returned + * @param integer $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 .= ' ' . $limit; + if (is_int($offset) && $offset > 0) { + $rt = ' OFFSET ' . intval($offset) . ' ROWS FETCH FIRST ' . intval($limit) . ' ROWS ONLY'; + } + 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, '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', 'decimal', 'numeric'))) { + return 'float'; + } + 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); + } + return $length->Length; + } + return parent::length($length); + } + +/** + * Builds a map of the columns contained in a result + * + * @param PDOStatement $results + * @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); + + 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 ' . intval($limitOffset[2]); + $page = intval($limitOffset[1] / $limitOffset[2]); + $offset = intval($limitOffset[2] * $page); + + $rowCounter = self::ROW_COUNTER; + return " + SELECT {$limit} * FROM ( + SELECT {$fields}, ROW_NUMBER() OVER ({$order}) AS {$rowCounter} + FROM {$table} {$alias} {$joins} {$conditions} {$group} + ) AS _cake_paging_ + WHERE _cake_paging_.{$rowCounter} > {$offset} + ORDER BY _cake_paging_.{$rowCounter} + "; + } elseif (strpos($limit, 'FETCH') !== false) { + return "SELECT {$fields} FROM {$table} {$alias} {$joins} {$conditions} {$group} {$order} {$limit}"; + } else { + return "SELECT {$limit} {$fields} FROM {$table} {$alias} {$joins} {$conditions} {$group} {$order}"; + } + break; + 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 "CREATE TABLE {$table} (\n{$columns});\n{$indexes}"; + break; + default: + return parent::renderStatement($type, $data); + break; + } + } + +/** + * 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 + * @return string Quoted and escaped data + */ + public function value($data, $column = null) { + if (is_array($data) || is_object($data)) { + return parent::value($data, $column); + } elseif (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); + } + } + +/** + * Returns an array of all result rows for a given SQL query. + * Returns false if no rows matched. + * + * @param Model $model + * @param array $queryData + * @param integer $recursive + * @return array 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 === self::ROW_COUNTER) { + continue; + } + $resultRow[$table][$column] = $row[$col]; + if ($type === 'boolean' && !is_null($row[$col])) { + $resultRow[$table][$column] = $this->boolean($resultRow[$table][$column]); + } + } + return $resultRow; + } + $this->_result->closeCursor(); + return false; + } + +/** + * Inserts multiple values into a table + * + * @param string $table + * @param string $fields + * @param array $values + * @return void + */ + public function insertMulti($table, $fields, $values) { + $primaryKey = $this->_getPrimaryKey($table); + $hasPrimaryKey = $primaryKey != null && ( + (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('/(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 + * @param string $table + * @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 + * @return integer 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; + 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 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 = ''; + foreach ($schema->tables as $curTable => $columns) { + if (!$table || $table == $curTable) { + $out .= "IF OBJECT_ID('" . $this->fullTableName($curTable, false) . "', 'U') IS NOT NULL DROP TABLE " . $this->fullTableName($curTable) . ";\n"; + } + } + return $out; + } + +/** + * Gets the schema name + * + * @return string The schema name + */ + public function getSchemaName() { + return $this->config['schema']; + } + +} diff --git a/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Datasource/DboSource.php b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Datasource/DboSource.php new file mode 100644 index 0000000..ce19daa --- /dev/null +++ b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Datasource/DboSource.php @@ -0,0 +1,3268 @@ + '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::conditions() will be stored here. Method caching uses `md5()`. If you have + * problems with collisions, set DboSource::$cacheMethods to false. + * + * @var array + */ + public static $methodCache = array(); + +/** + * Whether or not to cache the results of DboSource::name() and DboSource::conditions() + * into the memory cache. Set to false to disable the use of the memory cache. + * + * @var boolean. + */ + 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 boolean + */ + public $useNestedTransactions = false; + +/** + * Print full query debug info? + * + * @var boolean + */ + 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 integer + */ + public $numRows = null; + +/** + * Time the last query took + * + * @var integer + */ + public $took = null; + +/** + * Result + * + * @var array + */ + protected $_result = null; + +/** + * Queries count. + * + * @var integer + */ + protected $_queriesCnt = 0; + +/** + * Total duration of all queries. + * + * @var integer + */ + 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 integer Maximum number of queries in the queries log. + */ + protected $_queriesLogMax = 200; + +/** + * Caches serialized results of executed queries + * + * @var array Cache of results from executed sql queries. + */ + 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', 'or', 'not', 'in', 'between', 'regexp', 'similar to'); + +/** + * Indicates the level of nested transactions + * + * @var integer + */ + 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 + ); + +/** + * 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 boolean + */ + protected $_methodCacheChange = false; + +/** + * Constructor + * + * @param array $config Array of configuration information for the Datasource. + * @param boolean $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), + 'enabled' => false + )); + } + if ($autoConnect) { + $this->connect(); + } + } + +/** + * Reconnects to database server with optional new settings + * + * @param array $config An array defining the new configuration settings + * @return boolean 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 boolean True if the database could be disconnected, else false + */ + public function disconnect() { + if ($this->_result instanceof PDOStatement) { + $this->_result->closeCursor(); + } + unset($this->_connection); + $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 into which this data will be inserted + * @return string Quoted and escaped data + */ + public function value($data, $column = null) { + 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); + } + + switch ($column) { + case 'binary': + return $this->_connection->quote($data, PDO::PARAM_LOB); + break; + case 'boolean': + return $this->_connection->quote($this->boolean($data, true), PDO::PARAM_BOOL); + break; + case 'string': + case 'text': + return $this->_connection->quote($data, PDO::PARAM_STR); + default: + if ($data === '') { + return '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) + ) { + return $data; + } + return $this->_connection->quote($data); + break; + } + } + +/** + * 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 boolean + */ + public function rawQuery($sql, $params = array()) { + $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 + * @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)/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->_results = $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 + * @return integer 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 integer 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 && (strpos($args[0], 'findBy') === 0 || strpos($args[0], 'findAllBy') === 0)) { + $params = $args[1]; + + if (substr($args[0], 0, 6) === 'findBy') { + $all = false; + $field = Inflector::underscore(substr($args[0], 6)); + } else { + $all = true; + $field = Inflector::underscore(substr($args[0], 9)); + } + + $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 ($all) { + 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('all', compact('conditions', 'fields', 'order', 'limit', 'page', 'recursive')); + } else { + if (isset($params[3 + $off])) { + $recursive = $params[3 + $off]; + } + return $args[2]->find('first', compact('conditions', 'fields', 'order', 'recursive')); + } + } else { + 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)); + } + } + } + +/** + * 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; + } else { + 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 $params parameters to be bound as values for the SQL statement + * @param array $options additional options for the query. + * @return 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; + } + if ($result = $this->execute($sql, array(), $params)) { + $out = array(); + + if ($this->hasResult()) { + $first = $this->fetchRow(); + if ($first != null) { + $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 boolean + */ + 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 Name of the field + * @param string $sql SQL query + * @return mixed Value of field read. + */ + 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; + self::$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 (empty(self::$methodCache)) { + self::$methodCache = Cache::read('method_cache', '_cake_core_'); + } + if ($value === null) { + return (isset(self::$methodCache[$method][$key])) ? self::$methodCache[$method][$key] : null; + } + $this->_methodCacheChange = true; + return self::$methodCache[$method][$key] = $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 = md5($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 boolean True if the database is connected, else false + */ + public function isConnected() { + return $this->connected; + } + +/** + * Checks if the result is valid + * + * @return boolean True if the result is valid else false + */ + public function hasResult() { + return is_a($this->_result, 'PDOStatement'); + } + +/** + * Get the query log as an array. + * + * @param boolean $sorted Get the queries sorted by time taken, defaults to false. + * @param boolean $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 boolean $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('logs', 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_pop($this->_queriesLog); + } + } + +/** + * Gets full table name including prefix + * + * @param Model|string $model Either a Model object or a string table name. + * @param boolean $quote Whether you want the table name quoted. + * @param boolean $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 (false == strstr($table, '.')) { + return $this->name($schemaName) . '.' . $this->name($table); + } + } + return $this->name($table); + } + if ($schema && !empty($schemaName)) { + if (false == strstr($table, '.')) { + 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 boolean Success + */ + public function create(Model $model, $fields = null, $values = null) { + $id = null; + + if ($fields == null) { + unset($fields, $values); + $fields = array_keys($model->data); + $values = array_values($model->data); + } + $count = count($fields); + + for ($i = 0; $i < $count; $i++) { + $valueInsert[] = $this->value($values[$i], $model->getColumnType($fields[$i])); + $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 integer $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); + + $null = null; + $array = array('callbacks' => $queryData['callbacks']); + $linkedModels = array(); + $bypass = false; + + if ($recursive === null && isset($queryData['recursive'])) { + $recursive = $queryData['recursive']; + } + + if (!is_null($recursive)) { + $_recursive = $model->recursive; + $model->recursive = $recursive; + } + + if (!empty($queryData['fields'])) { + $bypass = true; + $queryData['fields'] = $this->fields($model, null, $queryData['fields']); + } else { + $queryData['fields'] = $this->fields($model); + } + + $_associations = $model->associations(); + + if ($model->recursive == -1) { + $_associations = array(); + } elseif ($model->recursive == 0) { + unset($_associations[2], $_associations[3]); + } + + foreach ($_associations as $type) { + foreach ($model->{$type} as $assoc => $assocData) { + $linkModel = $model->{$assoc}; + $external = isset($assocData['external']); + + $linkModel->getDataSource(); + if ($model->useDbConfig === $linkModel->useDbConfig) { + if ($bypass) { + $assocData['fields'] = false; + } + if (true === $this->generateAssociationQuery($model, $linkModel, $type, $assoc, $assocData, $queryData, $external, $null)) { + $linkedModels[$type . '/' . $assoc] = true; + } + } + } + } + + $query = trim($this->generateAssociationQuery($model, null, null, null, null, $queryData, false, $null)); + + $resultSet = $this->fetchAll($query, $model->cacheQueries); + + if ($resultSet === false) { + $model->onError(); + return false; + } + + $filtered = array(); + + if ($queryData['callbacks'] === true || $queryData['callbacks'] === 'after') { + $filtered = $this->_filterResults($resultSet, $model); + } + + 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])) { + if ($model->useDbConfig === $linkModel->useDbConfig) { + $db = $this; + } else { + $db = ConnectionManager::getDataSource($linkModel->useDbConfig); + } + } elseif ($model->recursive > 1 && ($type === 'belongsTo' || $type === 'hasOne')) { + $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') { + $filtered[] = $assoc; + } + } + } + } + if ($queryData['callbacks'] === true || $queryData['callbacks'] === 'after') { + $this->_filterResults($resultSet, $model, $filtered); + } + } + + if (!is_null($recursive)) { + $model->recursive = $_recursive; + } + return $resultSet; + } + +/** + * Passes association results thru afterFind filters of corresponding model + * + * @param array $results 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(&$results, Model $model, $filtered = array()) { + $current = reset($results); + 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 ($results as &$result) { + $data = $linkedModel->afterFind(array(array($className => $result[$className])), false); + if (isset($data[0][$className])) { + $result[$className] = $data[0][$className]; + } + } + } + return $filtering; + } + +/** + * Queries associations. Used to fetch results on recursive models. + * + * @param Model $model Primary Model object + * @param Model $linkModel Linked model that + * @param string $type Association type, one of the model association types ie. hasMany + * @param string $association + * @param array $assocData + * @param array $queryData + * @param boolean $external Whether or not the association query is on an external datasource. + * @param array $resultSet Existing results + * @param integer $recursive Number of levels of association + * @param array $stack + * @return mixed + * @throws CakeException when results cannot be created. + */ + public function queryAssociation(Model $model, &$linkModel, $type, $association, $assocData, &$queryData, $external, &$resultSet, $recursive, $stack) { + if (isset($stack['_joined'])) { + $joined = $stack['_joined']; + unset($stack['_joined']); + } + + if ($query = $this->generateAssociationQuery($model, $linkModel, $type, $association, $assocData, $queryData, $external, $resultSet)) { + 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'])) { + $ins = $fetch = array(); + foreach ($resultSet as &$result) { + if ($in = $this->insertQueryData('{$__cakeID__$}', $result, $association, $assocData, $model, $linkModel, $stack)) { + $ins[] = $in; + } + } + + if (!empty($ins)) { + $ins = array_unique($ins); + $fetch = $this->fetchAssociated($model, $query, $ins); + } + + if (!empty($fetch) && is_array($fetch)) { + if ($recursive > 0) { + foreach ($linkModel->associations() as $type1) { + foreach ($linkModel->{$type1} as $assoc1 => $assocData1) { + $deepModel = $linkModel->{$assoc1}; + $tmpStack = $stack; + $tmpStack[] = $assoc1; + + if ($linkModel->useDbConfig === $deepModel->useDbConfig) { + $db = $this; + } else { + $db = ConnectionManager::getDataSource($deepModel->useDbConfig); + } + $db->queryAssociation($linkModel, $deepModel, $type1, $assoc1, $assocData1, $queryData, true, $fetch, $recursive - 1, $tmpStack); + } + } + } + } + if ($queryData['callbacks'] === true || $queryData['callbacks'] === 'after') { + $this->_filterResults($fetch, $model); + } + return $this->_mergeHasMany($resultSet, $fetch, $association, $model, $linkModel); + } elseif ($type === 'hasAndBelongsToMany') { + $ins = $fetch = array(); + foreach ($resultSet as &$result) { + if ($in = $this->insertQueryData('{$__cakeID__$}', $result, $association, $assocData, $model, $linkModel, $stack)) { + $ins[] = $in; + } + } + if (!empty($ins)) { + $ins = array_unique($ins); + if (count($ins) > 1) { + $query = str_replace('{$__cakeID__$}', '(' . implode(', ', $ins) . ')', $query); + $query = str_replace('= (', 'IN (', $query); + } else { + $query = str_replace('{$__cakeID__$}', $ins[0], $query); + } + $query = str_replace(' WHERE 1 = 1', '', $query); + } + + $foreignKey = $model->hasAndBelongsToMany[$association]['foreignKey']; + $joinKeys = array($foreignKey, $model->hasAndBelongsToMany[$association]['associationForeignKey']); + list($with, $habtmFields) = $model->joinModel($model->hasAndBelongsToMany[$association]['with'], $joinKeys); + $habtmFieldsCount = count($habtmFields); + $q = $this->insertQueryData($query, null, $association, $assocData, $model, $linkModel, $stack); + + if ($q !== false) { + $fetch = $this->fetchAll($q, $model->cacheQueries); + } else { + $fetch = null; + } + } + + $modelAlias = $model->alias; + $modelPK = $model->primaryKey; + foreach ($resultSet as &$row) { + if ($type !== 'hasAndBelongsToMany') { + $q = $this->insertQueryData($query, $row, $association, $assocData, $model, $linkModel, $stack); + $fetch = null; + if ($q !== false) { + $joinedData = array(); + if (($type === 'belongsTo' || $type === 'hasOne') && isset($row[$linkModel->alias], $joined[$model->alias]) && in_array($linkModel->alias, $joined[$model->alias])) { + $joinedData = Hash::filter($row[$linkModel->alias]); + if (!empty($joinedData)) { + $fetch[0] = array($linkModel->alias => $row[$linkModel->alias]); + } + } else { + $fetch = $this->fetchAll($q, $model->cacheQueries); + } + } + } + $selfJoin = $linkModel->name === $model->name; + + if (!empty($fetch) && is_array($fetch)) { + if ($recursive > 0) { + foreach ($linkModel->associations() as $type1) { + foreach ($linkModel->{$type1} as $assoc1 => $assocData1) { + $deepModel = $linkModel->{$assoc1}; + + if ($type1 === 'belongsTo' || ($deepModel->alias === $modelAlias && $type === 'belongsTo') || ($deepModel->alias !== $modelAlias)) { + $tmpStack = $stack; + $tmpStack[] = $assoc1; + if ($linkModel->useDbConfig == $deepModel->useDbConfig) { + $db = $this; + } else { + $db = ConnectionManager::getDataSource($deepModel->useDbConfig); + } + $db->queryAssociation($linkModel, $deepModel, $type1, $assoc1, $assocData1, $queryData, true, $fetch, $recursive - 1, $tmpStack); + } + } + } + } + if ($type === 'hasAndBelongsToMany') { + $uniqueIds = $merge = array(); + + foreach ($fetch as $j => $data) { + if (isset($data[$with]) && $data[$with][$foreignKey] === $row[$modelAlias][$modelPK]) { + 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 { + $this->_mergeAssociation($row, $fetch, $association, $type, $selfJoin); + } + if (isset($row[$association])) { + $row[$association] = $linkModel->afterFind($row[$association], false); + } + } else { + $tempArray[0][$association] = false; + $this->_mergeAssociation($row, $tempArray, $association, $type, $selfJoin); + } + } + } + } + +/** + * A more efficient way to fetch associations. Woohoo! + * + * @param Model $model Primary model object + * @param string $query Association query + * @param array $ids Array of IDs of associated records + * @return array Association results + */ + public function fetchAssociated(Model $model, $query, $ids) { + $query = str_replace('{$__cakeID__$}', implode(', ', $ids), $query); + if (count($ids) > 1) { + $query = str_replace('= (', 'IN (', $query); + } + return $this->fetchAll($query, $model->cacheQueries); + } + +/** + * mergeHasMany - Merge the results of hasMany relations. + * + * + * @param array $resultSet Data to merge into + * @param array $merge Data to merge + * @param string $association Name of Model being Merged + * @param Model $model Model being merged onto + * @param Model $linkModel Model being merged + * @return void + */ + protected function _mergeHasMany(&$resultSet, $merge, $association, $model, $linkModel) { + $modelAlias = $model->alias; + $modelPK = $model->primaryKey; + $modelFK = $model->hasMany[$association]['foreignKey']; + foreach ($resultSet as &$result) { + if (!isset($result[$modelAlias])) { + continue; + } + $merged = array(); + foreach ($merge as $data) { + if ($result[$modelAlias][$modelPK] === $data[$association][$modelFK]) { + 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]); + } + } + $merged[] = $data; + } else { + $merged[] = $data[$association]; + } + } + } + $result = Hash::mergeDiff($result, array($association => $merged)); + } + } + +/** + * Merge association of merge into data + * + * @param array $data + * @param array $merge + * @param string $association + * @param string $type + * @param boolean $selfJoin + * @return void + */ + protected function _mergeAssociation(&$data, &$merge, $association, $type, $selfJoin = false) { + if (isset($merge[0]) && !isset($merge[0][$association])) { + $association = Inflector::pluralize($association); + } + + if ($type === 'belongsTo' || $type === 'hasOne') { + if (isset($merge[$association])) { + $data[$association] = $merge[$association][0]; + } else { + if (count($merge[0][$association]) > 1) { + foreach ($merge[0] as $assoc => $data2) { + if ($assoc !== $association) { + $merge[0][$association][$assoc] = $data2; + } + } + } + if (!isset($data[$association])) { + if ($merge[0][$association] != null) { + $data[$association] = $merge[0][$association]; + } else { + $data[$association] = array(); + } + } else { + if (is_array($merge[0][$association])) { + foreach ($data[$association] 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) { + $data[$association][$association] = $merge[0][$association]; + } else { + $diff = Hash::diff($dataAssocTmp, $mergeAssocTmp); + $data[$association] = array_merge($merge[0][$association], $diff); + } + } elseif ($selfJoin && array_key_exists($association, $merge[0])) { + $data[$association] = array_merge($data[$association], array($association => array())); + } + } + } + } else { + if (isset($merge[0][$association]) && $merge[0][$association] === false) { + if (!isset($data[$association])) { + $data[$association] = array(); + } + } else { + foreach ($merge as $i => $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($data[$association]) || (isset($data[$association]) && !in_array($insert, $data[$association], true))) { + $data[$association][] = $insert; + } + } + } + } + } + +/** + * Generates an array representing a query or part of a query from a single model or two associated models + * + * @param Model $model + * @param Model $linkModel + * @param string $type + * @param string $association + * @param array $assocData + * @param array $queryData + * @param boolean $external + * @param array $resultSet + * @return mixed + */ + public function generateAssociationQuery(Model $model, $linkModel, $type, $association, $assocData, &$queryData, $external, &$resultSet) { + $queryData = $this->_scrubQueryData($queryData); + $assocData = $this->_scrubQueryData($assocData); + $modelAlias = $model->alias; + + if (empty($queryData['fields'])) { + $queryData['fields'] = $this->fields($model, $modelAlias); + } elseif (!empty($model->hasMany) && $model->recursive > -1) { + $assocFields = $this->fields($model, $modelAlias, array("{$modelAlias}.{$model->primaryKey}")); + $passedFields = $queryData['fields']; + if (count($passedFields) === 1) { + if (strpos($passedFields[0], $assocFields[0]) === false && !preg_match('/^[a-z]+\(/i', $passedFields[0])) { + $queryData['fields'] = array_merge($passedFields, $assocFields); + } else { + $queryData['fields'] = $passedFields; + } + } else { + $queryData['fields'] = array_merge($passedFields, $assocFields); + } + unset($assocFields, $passedFields); + } + + if ($linkModel === null) { + return $this->buildStatement( + array( + 'fields' => array_unique($queryData['fields']), + 'table' => $this->fullTableName($model), + 'alias' => $modelAlias, + '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']; + } + + $self = $model->name === $linkModel->name; + $fields = array(); + + if ($external || (in_array($type, array('hasOne', 'belongsTo')) && $assocData['fields'] !== false)) { + $fields = $this->fields($linkModel, $association, $assocData['fields']); + } + if (empty($assocData['offset']) && !empty($assocData['page'])) { + $assocData['offset'] = ($assocData['page'] - 1) * $assocData['limit']; + } + $assocData['limit'] = $this->limit($assocData['limit'], $assocData['offset']); + + switch ($type) { + case 'hasOne': + case 'belongsTo': + $conditions = $this->_mergeConditions( + $assocData['conditions'], + $this->getConstraint($type, $model, $linkModel, $association, array_merge($assocData, compact('external', 'self'))) + ); + + if (!$self && $external) { + foreach ($conditions as $key => $condition) { + if (is_numeric($key) && strpos($condition, $modelAlias . '.') !== false) { + unset($conditions[$key]); + } + } + } + + if ($external) { + $query = array_merge($assocData, array( + 'conditions' => $conditions, + 'table' => $this->fullTableName($linkModel), + 'fields' => $fields, + 'alias' => $association, + 'group' => null + )); + $query += array('order' => $assocData['order'], 'limit' => $assocData['limit']); + } else { + $join = array( + 'table' => $linkModel, + 'alias' => $association, + 'type' => isset($assocData['type']) ? $assocData['type'] : 'LEFT', + 'conditions' => trim($this->conditions($conditions, true, false, $model)) + ); + $queryData['fields'] = array_merge($queryData['fields'], $fields); + + if (!empty($assocData['order'])) { + $queryData['order'][] = $assocData['order']; + } + if (!in_array($join, $queryData['joins'])) { + $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'], + '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 = $model->{$with}->alias; + $joinFields = $this->fields($model->{$with}, $joinAlias, $joinFields); + } else { + $joinFields = array(); + } + } else { + $joinTbl = $assocData['joinTable']; + $joinAlias = $this->fullTableName($assocData['joinTable']); + } + $query = array( + 'conditions' => $assocData['conditions'], + 'limit' => $assocData['limit'], + '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 Model object + * @param string $linkModel + * @param string $alias + * @param array $assoc + * @param string $alias2 + * @return array Conditions array defining the constraint between $model and $association + */ + public function getConstraint($type, $model, $linkModel, $alias, $assoc, $alias2 = null) { + $assoc += array('external' => false, 'self' => false); + + if (empty($assoc['foreignKey'])) { + return array(); + } + + switch (true) { + case ($assoc['external'] && $type === 'hasOne'): + return array("{$alias}.{$assoc['foreignKey']}" => '{$__cakeID__$}'); + case ($assoc['external'] && $type === 'belongsTo'): + return array("{$alias}.{$linkModel->primaryKey}" => '{$__cakeForeignKey__$}'); + case (!$assoc['external'] && $type === 'hasOne'): + return array("{$alias}.{$assoc['foreignKey']}" => $this->identifier("{$model->alias}.{$model->primaryKey}")); + case (!$assoc['external'] && $type === 'belongsTo'): + return array("{$model->alias}.{$assoc['foreignKey']}" => $this->identifier("{$alias}.{$linkModel->primaryKey}")); + case ($type === 'hasMany'): + return array("{$alias}.{$assoc['foreignKey']}" => array('{$__cakeID__$}')); + case ($type === 'hasAndBelongsToMany'): + return array( + array("{$alias}.{$assoc['foreignKey']}" => '{$__cakeID__$}'), + array("{$alias}.{$assoc['associationForeignKey']}" => $this->identifier("{$alias2}.{$linkModel->primaryKey}")) + ); + } + return array(); + } + +/** + * Builds and generates a JOIN statement from an array. Handles final clean-up before conversion. + * + * @param array $join An array defining a JOIN statement in a query + * @return string An SQL JOIN statement 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' => array() + ), $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'])) { + $schema = !(is_string($data['table']) && strpos($data['table'], '(') === 0); + $data['table'] = $this->fullTableName($data['table'], true, $schema); + } + 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) { + $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) + )); + } + +/** + * Renders a final SQL JOIN statement + * + * @param array $data + * @return string + */ + public function renderJoinStatement($data) { + extract($data); + return trim("{$type} JOIN {$table} {$alias} ON ({$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 Rendered SQL expression to be run. + */ + public function renderStatement($type, $data) { + extract($data); + $aliases = null; + + switch (strtolower($type)) { + case 'select': + return "SELECT {$fields} FROM {$table} {$alias} {$joins} {$conditions} {$group} {$order} {$limit}"; + case 'create': + return "INSERT INTO {$table} ({$fields}) VALUES ({$values})"; + case 'update': + if (!empty($alias)) { + $aliases = "{$this->alias}{$alias} {$joins} "; + } + return "UPDATE {$table} {$aliases}SET {$fields} {$conditions}"; + case 'delete': + if (!empty($alias)) { + $aliases = "{$this->alias}{$alias} {$joins} "; + } + return "DELETE {$alias} FROM {$table} {$aliases}{$conditions}"; + case 'schema': + foreach (array('columns', 'indexes', 'tableParameters') as $var) { + if (is_array(${$var})) { + ${$var} = "\t" . join(",\n\t", array_filter(${$var})); + } else { + ${$var} = ''; + } + } + if (trim($indexes) !== '') { + $columns .= ','; + } + return "CREATE TABLE {$table} (\n{$columns}{$indexes}) {$tableParameters};"; + case 'alter': + return; + } + } + +/** + * Merges a mixed set of string/array conditions + * + * @param mixed $query + * @param mixed $assoc + * @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 + * @param array $fields + * @param array $values + * @param mixed $conditions + * @return boolean Success + */ + public function update(Model $model, $fields = array(), $values = null, $conditions = null) { + if ($values == null) { + $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 + * @param array $fields + * @param boolean $quoteValues If values should be quoted, or treated as SQL snippets + * @param boolean $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; + + $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)); + } 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 + * @param mixed $conditions + * @return boolean 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 + * @param mixed $conditions + * @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($alias, $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 fragments from a model's associations + * + * @param Model $model + * @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}) && $model->useDbConfig == $model->{$assoc}->useDbConfig && $model->{$assoc}->getDataSource()) { + $assocData = $model->getAssociated($assoc); + $join[] = $this->buildJoinStatement(array( + 'table' => $model->{$assoc}, + 'alias' => $assoc, + 'type' => isset($assocData['type']) ? $assocData['type'] : 'LEFT', + 'conditions' => trim($this->conditions( + $this->_mergeConditions($assocData['conditions'], $this->getConstraint($assocData['association'], $model, $model->{$assoc}, $assoc, $assocData)), + true, false, $model + )) + )); + } + } + return $join; + } + +/** + * Returns an SQL calculation, i.e. COUNT() or MAX() + * + * @param Model $model + * @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 (is_object($model) && $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 (is_object($model) && $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]); + break; + } + } + +/** + * 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 boolean 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 boolean + */ + public function nestedTransactionSupported() { + return false; + } + +/** + * Begin a transaction + * + * @return boolean 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->logQuery('BEGIN'); + } + return $this->_transactionStarted = $this->_connection->beginTransaction(); + } + +/** + * Begin a nested transaction + * + * @return boolean + */ + protected function _beginNested() { + $query = 'SAVEPOINT LEVEL' . ++$this->_transactionNesting; + if ($this->fullDebug) { + $this->logQuery($query); + } + $this->_connection->exec($query); + return true; + } + +/** + * Commit a transaction + * + * @return boolean 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->logQuery('COMMIT'); + } + $this->_transactionStarted = false; + return $this->_connection->commit(); + } + + if ($this->nestedTransactionSupported()) { + return $this->_commitNested(); + } + + $this->_transactionNesting--; + return true; + } + +/** + * Commit a nested transaction + * + * @return boolean + */ + protected function _commitNested() { + $query = 'RELEASE SAVEPOINT LEVEL' . $this->_transactionNesting--; + if ($this->fullDebug) { + $this->logQuery($query); + } + $this->_connection->exec($query); + return true; + } + +/** + * Rollback a transaction + * + * @return boolean 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->logQuery('ROLLBACK'); + } + $this->_transactionStarted = false; + return $this->_connection->rollBack(); + } + + if ($this->nestedTransactionSupported()) { + return $this->_rollbackNested(); + } + + $this->_transactionNesting--; + return true; + } + +/** + * Rollback a nested transaction + * + * @return boolean + */ + protected function _rollbackNested() { + $query = 'ROLLBACK TO SAVEPOINT LEVEL' . $this->_transactionNesting--; + if ($this->fullDebug) { + $this->logQuery($query); + } + $this->_connection->exec($query); + return true; + } + +/** + * Returns the ID generated from the previous INSERT operation. + * + * @param mixed $source + * @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 + * @param string|array|boolean $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 boolean $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(); + if (!$exists && $conditions !== null) { + 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 + * @param string $key + * @param string $assoc + * @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 + * @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['callbacks'] = null; + } + return (array)$data + $base; + } + +/** + * Converts model virtual fields into sql expressions to be fetched later + * + * @param Model $model + * @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); + $expression = $this->_quoteFields($model->getVirtualField($field)); + $virtual[] = '(' . $expression . ") {$this->alias} {$virtualField}"; + } + return $virtual; + } + +/** + * Generates the fields list of an SQL query. + * + * @param Model $model + * @param string $alias Alias table name + * @param mixed $fields + * @param boolean $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) + ); + $cacheKey = md5(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 = String::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 boolean $quoteValues If true, values should be quoted + * @param boolean $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 = 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, $match)) { + $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 boolean $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 = null) { + $out = array(); + $data = $columnType = null; + $bool = array('and', 'or', 'not', 'and not', 'or not', 'xor', '||', '&&'); + + 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[] = $not . $this->_quoteFields($value); + } elseif ((is_numeric($key) && is_array($value)) || in_array(strtolower(trim($key)), $bool)) { + if (in_array(strtolower(trim($key)), $bool)) { + $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[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)) { + $count = count($value); + if ($count === 1 && !preg_match("/\s+NOT$/", $key)) { + $data = $this->_quoteFields($key) . ' = ('; + } else { + $data = $this->_quoteFields($key) . ' IN ('; + } + if ($quoteValues) { + if (is_object($model)) { + $columnType = $model->getColumnType($key); + } + $data .= implode(', ', $this->value($value, $columnType)); + } + $data .= ')'; + } 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($model, trim($key), $value); + } + + if ($data != null) { + $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 Model $model Model object initiating the query + * @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 + * @return string + */ + protected function _parseKey($model, $key, $value) { + $operatorMatch = '/^(((' . implode(')|(', $this->_sqlOps); + $operatorMatch .= ')\\x20?)|<[>=]?(?![^>]+>)\\x20?|[>=!]{1,3}(?!<)\\x20?)/is'; + $bound = (strpos($key, '?') !== false || (is_array($value) && strpos($key, ':') !== false)); + + if (strpos($key, ' ') === false) { + $operator = '='; + } else { + list($key, $operator) = explode(' ', trim($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; + if (is_object($model) && $model->isVirtualField($key)) { + $key = $this->_quoteFields($model->getVirtualField($key)); + $virtual = true; + } + + $type = is_object($model) ? $model->getColumnType($key) : null; + $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); + $key = $isKey ? $this->_quoteFields($key) : $this->name($key); + } + + if ($bound) { + return String::insert($key . ' ' . trim($operator), $value); + } + + if (!preg_match($operatorMatch, trim($operator))) { + $operator .= ' ='; + } + $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 + * @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); + } + $conditions = str_replace(array($start, $end), '', $conditions); + $conditions = preg_replace_callback('/(?:[\'\"][^\'\"\\\]*(?:\\\.[^\'\"\\\]*)*[\'\"])|([a-z0-9_' . $start . $end . ']*\\.[a-z0-9_' . $start . $end . ']*)/i', array(&$this, '_quoteMatchedField'), $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 integer $limit Limit of results returned + * @param integer $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), 'limit')) { + $rt = ' LIMIT'; + } + + if ($offset) { + $rt .= ' ' . $offset . ','; + } + + $rt .= ' ' . $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 = null) { + if (!is_array($keys)) { + $keys = array($keys); + } + $keys = array_filter($keys); + $result = array(); + while (!empty($keys)) { + list($key, $dir) = each($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 (is_object($model) && $model->isVirtualField($key)) { + $key = '(' . $this->_quoteFields($model->getVirtualField($key)) . ')'; + } + list($alias, $field) = pluginSplit($key); + if (is_object($model) && $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 $group Group By Condition + * @param Model $model + * @return string string condition or null + */ + public function group($group, $model = null) { + if ($group) { + if (!is_array($group)) { + $group = array($group); + } + foreach ($group as $index => $key) { + if (is_object($model) && $model->isVirtualField($key)) { + $group[$index] = '(' . $model->getVirtualField($key) . ')'; + } + } + $group = implode(', ', $group); + return ' GROUP BY ' . $this->_quoteFields($group); + } + return null; + } + +/** + * 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 boolean 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) { + if (!preg_match_all('/([\w\s]+)(?:\((\d+)(?:,(\d+))?\))?(\sunsigned)?(\szerofill)?/', $real, $result)) { + $col = str_replace(array(')', 'unsigned'), '', $real); + $limit = null; + + if (strpos($col, '(') !== false) { + list($col, $limit) = explode('(', $col); + } + if ($limit !== null) { + return intval($limit); + } + return null; + } + + $types = array( + 'int' => 1, 'tinyint' => 1, 'smallint' => 1, 'mediumint' => 1, 'integer' => 1, 'bigint' => 1 + ); + + list($real, $type, $length, $offset, $sign, $zerofill) = $result; + $typeArr = $type; + $type = $type[0]; + $length = $length[0]; + $offset = $offset[0]; + + $isFloat = in_array($type, array('dec', 'decimal', 'float', 'numeric', 'double')); + if ($isFloat && $offset) { + return $length . ',' . $offset; + } + + if (($real[0] == $type) && (count($real) === 1)) { + return null; + } + + if (isset($types[$type])) { + $length += $types[$type]; + if (!empty($sign)) { + $length--; + } + } elseif (in_array($type, array('enum', 'set'))) { + $length = 0; + foreach ($typeArr as $key => $enumValue) { + if ($key === 0) { + continue; + } + $tmpLength = strlen($enumValue); + if ($tmpLength > $length) { + $length = $tmpLength; + } + } + } + return intval($length); + } + +/** + * Translates between PHP boolean values and Database (faked) boolean values + * + * @param mixed $data Value to be translated + * @param boolean $quote + * @return string|boolean 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 boolean + */ + 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 $row => $value) { + $i = 1; + foreach ($value as $col => $val) { + $statement->bindValue($i, $val, $columnMap[$col]); + $i += 1; + } + $statement->execute(); + $statement->closeCursor(); + } + return $this->commit(); + } + +/** + * 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 false; + } + +/** + * Generate a database-native schema for the given Schema object + * + * @param Model $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 (!is_a($schema, '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 = $colList = $indexes = $tableParameters = array(); + $primary = null; + $table = $this->fullTableName($curTable); + + foreach ($columns as $name => $col) { + if (is_string($col)) { + $col = array('type' => $col); + } + if (isset($col['key']) && $col['key'] === 'primary') { + $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 (empty($indexes) && !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 a alter syntax from CakeSchema::compare() + * + * @param mixed $compare + * @param string $table + * @return boolean + */ + 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 = ''; + + foreach ($schema->tables as $curTable => $columns) { + if (!$table || $table == $curTable) { + $out .= 'DROP TABLE ' . $this->fullTableName($curTable) . ";\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(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])) { + trigger_error(__d('cake_dev', 'Column type %s does not exist', $type), E_USER_WARNING); + return null; + } + + $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') { + $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 ($type === 'timestamp' && 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'])) { + continue; + } + $val = $columnData[$paramName]; + if ($value['quote']) { + $val = $this->value($val); + } + $columnString .= ' ' . $value['value'] . $value['join'] . $val; + } + } + return $columnString; + } + +/** + * Format indexes for create table + * + * @param array $indexes + * @param string $table + * @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 + * @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 + * @param string $table + * @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 + * @return void + */ + public function introspectType($value) { + if (!is_array($value)) { + if (is_bool($value)) { + return 'boolean'; + } + if (is_float($value) && floatval($value) === $value) { + return 'float'; + } + if (is_int($value) && intval($value) === $value) { + return 'integer'; + } + if (is_string($value) && strlen($value) > 255) { + return 'text'; + } + return 'string'; + } + + $isAllFloat = $isAllInt = true; + $containsFloat = $containsInt = $containsString = false; + foreach ($value as $key => $valElement) { + $valElement = trim($valElement); + if (!is_float($valElement) && !preg_match('/^[\d]+\.[\d]+$/', $valElement)) { + $isAllFloat = false; + } else { + $containsFloat = true; + 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'; + } + +/** + * 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', self::$methodCache, '_cake_core_'); + } + } + +} diff --git a/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Datasource/Session/CacheSession.php b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Datasource/Session/CacheSession.php new file mode 100644 index 0000000..c9f14e4 --- /dev/null +++ b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Datasource/Session/CacheSession.php @@ -0,0 +1,90 @@ + '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 boolean Success + */ + public function open() { + return true; + } + +/** + * Method called on close of a database session. + * + * @return boolean Success + */ + public function close() { + return true; + } + +/** + * Method used to read from a database session. + * + * @param integer|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->primaryKey => $id) + )); + + if (empty($row[$this->_model->alias]['data'])) { + return false; + } + + return $row[$this->_model->alias]['data']; + } + +/** + * Helper function called on write for database sessions. + * + * @param integer $id ID that uniquely identifies session in database + * @param mixed $data The value of the data to be saved. + * @return boolean 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; + return $this->_model->save($record); + } + +/** + * Method called on the destruction of a database session. + * + * @param integer $id ID that uniquely identifies session in database + * @return boolean True for successful delete, false otherwise. + */ + public function destroy($id) { + return $this->_model->delete($id); + } + +/** + * Helper function called on gc for database sessions. + * + * @param integer $expires Timestamp (defaults to current time) + * @return boolean Success + */ + public function gc($expires = null) { + if (!$expires) { + $expires = time(); + } + return $this->_model->deleteAll(array($this->_model->alias . ".expires <" => $expires), false, false); + } + +} diff --git a/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/I18nModel.php b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/I18nModel.php new file mode 100644 index 0000000..9b18506 --- /dev/null +++ b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/I18nModel.php @@ -0,0 +1,44 @@ + table 'users'; class 'Man' => table 'men') + * The table is required to have at least 'id auto_increment' primary key. + * + * @package Cake.Model + * @link http://book.cakephp.org/2.0/en/models.html + */ +class Model extends Object 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 http://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 + * @link http://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 + * @link http://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 + * @link http://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 + * @access public + */ + 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 http://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' => 'notEmpty' + * ); + * }}} + * + * ### Validating using methods (with parameters) + * + * {{{ + * public $validate = array( + * 'age' => array( + * 'rule' => array('between', 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( + * 'age' => array( + * 'rule' => array('between', 5, 25), + * 'message' => array('The age must be between %d and %d.') + * ) + * ); + * }}} + * + * ### 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 http://book.cakephp.org/2.0/en/models/model-attributes.html#validate + * @link http://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 http://book.cakephp.org/2.0/en/models/model-attributes.html#tableprefix + */ + public $tablePrefix = null; + +/** + * Name of the model. + * + * @var string + * @link http://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 boolean + * @link http://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 classname 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 http://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 classname 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 http://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 classname 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 http://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 classname 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`, `deleteQuery`, `insertQuery`: A complete SQL query CakePHP + * can use to fetch, delete, or create new associated model records. This should + * be used in situations that require very custom results. + * + * @var array + * @link http://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. Eg: + * + * public $actsAs = array('Translate', 'MyBehavior' => array('setting1' => 'value1')) + * + * @var array + * @link http://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 boolean + */ + 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 integer + * @link http://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 http://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 http://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', 'deleteQuery', 'insertQuery') + ); + +/** + * Holds provided/generated association key names and other data for all associations. + * + * @var array + */ + protected $_associations = array('belongsTo', 'hasOne', 'hasMany', 'hasAndBelongsToMany'); + +/** + * 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(); + +/** + * The ID of the model record that was last inserted. + * + * @var integer + */ + protected $_insertID = null; + +/** + * Has the datasource been configured. + * + * @var boolean + * @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 integer|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) { + 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 + ), + $id + )); + } + + 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 == null) { + 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 you 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); + return $return; + } + +/** + * 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 boolean 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 boolean $reset Set to false to make the binding permanent + * @return boolean Success + * @link http://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')) );` + * + * 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 boolean $reset Set to false to make the unbinding permanent + * @return boolean Success + * @link http://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}; + } + 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) { + if (!is_array($this->{$type})) { + $this->{$type} = explode(',', $this->{$type}); + + foreach ($this->{$type} as $i => $className) { + $className = trim($className); + unset ($this->{$type}[$i]); + $this->{$type}[$className] = array(); + } + } + + if (!empty($this->{$type})) { + foreach ($this->{$type} as $assoc => $value) { + $plugin = null; + + if (is_numeric($assoc)) { + unset ($this->{$type}[$assoc]); + $assoc = $value; + $value = array(); + + if (strpos($assoc, '.') !== false) { + list($plugin, $assoc) = pluginSplit($assoc, true); + $this->{$type}[$assoc] = array('className' => $plugin . $assoc); + } else { + $this->{$type}[$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 + * @return void + */ + protected function _generateAssociation($type, $assocKey) { + $class = $assocKey; + $dynamicWith = false; + + foreach ($this->_associationKeys[$type] as $key) { + + if (!isset($this->{$type}[$assocKey][$key]) || $this->{$type}[$assocKey][$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($this->{$type}[$assocKey]['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; + } + $this->{$type}[$assocKey][$key] = $data; + } + + if ($dynamicWith) { + $this->{$type}[$assocKey]['dynamicWith'] = true; + } + + } + } + +/** + * Sets a custom table for your controller 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); + $db->cacheSources = ($this->cacheSources && $db->cacheSources); + + if (method_exists($db, 'listSources')) { + $sources = $db->listSources(); + 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, + )); + } + $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 $two Value string for the alternative indata method + * @return array Data with all of $one's keys and values + * @link http://book.cakephp.org/2.0/en/models/saving-your-data.html + */ + public function set($one, $two = null) { + if (!$one) { + return; + } + 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)) { + + foreach ($fieldSet as $fieldName => $fieldValue) { + if (isset($this->validationErrors[$fieldName])) { + unset ($this->validationErrors[$fieldName]); + } + + if ($modelName === $this->alias) { + if ($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 + * @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'))) { + $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 && + 'pm' == $data['meridian'] + ) { + $data['hour'] = $data['hour'] + 12; + } + if (isset($data['hour']) && isset($data['meridian']) && $data['hour'] == 12 && 'am' == $data['meridian']) { + $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]) || $data[$val][0] === '-')) { + 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 boolean|string $field Set to true to reload schema, or a string to return a specific field + * @return array 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->useTable !== false) { + $this->_schema = $db->describe($this); + } elseif ($this->useTable === false) { + $this->_schema = array(); + } + } + if (is_string($field)) { + if (isset($this->_schema[$field])) { + return $this->_schema[$field]; + } else { + return null; + } + } + return $this->_schema; + } + +/** + * 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) { + $db = $this->getDataSource(); + $cols = $this->schema(); + $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 ($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 boolean $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)) { + if ($this->isVirtualField($name)) { + return true; + } + } + + if (empty($this->_schema)) { + $this->schema(); + } + + if ($this->_schema != null) { + 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 boolean True on method being callable. + */ + public function hasMethod($method) { + if (method_exists($this, $method)) { + return true; + } + if ($this->Behaviors->hasMethod($method)) { + return true; + } + return false; + } + +/** + * Returns true if the supplied field is a model Virtual Field + * + * @param string $field Name of field to look for + * @return boolean 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 == null) { + return empty($this->virtualFields) ? false : $this->virtualFields; + } + if ($this->isVirtualField($field)) { + if (strpos($field, '.') !== false) { + list($model, $field) = explode('.', $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 boolean|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 boolean $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 http://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) { + foreach ($this->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; + } + +/** + * 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 integer|string $id The ID of the record to read + * @return array Array of database fields, or false if not found + * @link http://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 != null) { + $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; + } else { + return false; + } + } + +/** + * Returns the contents of a single field given the supplied conditions, in the + * supplied order. + * + * @param string $name Name of field to get + * @param array $conditions SQL conditions (defaults to NULL) + * @param string $order SQL ORDER BY fragment + * @return string field contents, or false if not found + * @link http://book.cakephp.org/2.0/en/models/retrieving-your-data.html#model-field + */ + public function field($name, $conditions = null, $order = null) { + if ($conditions === null && $this->id !== false) { + $conditions = array($this->alias . '.' . $this->primaryKey => $this->id); + } + if ($this->recursive >= 1) { + $recursive = -1; + } else { + $recursive = $this->recursive; + } + $fields = $name; + if ($data = $this->find('first', compact('conditions', 'fields', 'order', 'recursive'))) { + 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]); + } + } else { + return false; + } + } + +/** + * 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 array $validate See $options param in Model::save(). Does not respect 'fieldList' key if passed + * @return boolean See Model::save() + * @see Model::save() + * @link http://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); + + if (is_array($validate)) { + $options = array_merge(array('validate' => false, 'fieldList' => array($name)), $validate); + } else { + $options = array('validate' => $validate, '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. + * + * @param array $data Data to save. + * @param boolean|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 fieldList + * @param array $fieldList List of fields to allow to be written + * @return mixed On success Model::$data if its not empty or true, false on failure + * @link http://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); + $_whitelist = $this->whitelist; + $fields = array(); + + if (!is_array($validate)) { + $options = array_merge($defaults, compact('validate', 'fieldList', 'callbacks')); + } else { + $options = array_merge($defaults, $validate); + } + + if (!empty($options['fieldList'])) { + if (!empty($options['fieldList'][$this->alias]) && is_array($options['fieldList'][$this->alias])) { + $this->whitelist = $options['fieldList'][$this->alias]; + } else { + $this->whitelist = $options['fieldList']; + } + } elseif ($options['fieldList'] === null) { + $this->whitelist = array(); + } + $this->set($data); + + if (empty($this->data) && !$this->hasField(array('created', 'updated', 'modified'))) { + 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(); + $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(); + + foreach ($dateFields as $updateCol) { + if ($this->hasField($updateCol) && !in_array($updateCol, $fields)) { + $default = array('formatter' => 'date'); + $colType = array_merge($default, $db->columns[$this->getColumnType($updateCol)]); + if (!array_key_exists('format', $colType)) { + $time = strtotime('now'); + } else { + $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]); + } + $fields = $values = array(); + + 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 (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); + } + } + } + } + } + $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)) { + $success = (bool)$db->update($this, $fields, $values); + } else { + $fInfo = $this->schema($this->primaryKey); + $isUUID = ($fInfo['length'] == 36 && + ($fInfo['type'] === 'string' || $fInfo['type'] === 'binary') + ); + if (empty($this->data[$this->alias][$this->primaryKey]) && $isUUID) { + if (array_key_exists($this->primaryKey, $this->data[$this->alias])) { + $j = array_search($this->primaryKey, $fields); + $values[$j] = String::uuid(); + } else { + list($fields[], $values[]) = array($this->primaryKey, String::uuid()); + } + } + + if (!$db->create($this, $fields, $values)) { + $success = $created = false; + } else { + $created = true; + } + } + + if ($success && !empty($this->belongsTo)) { + $this->updateCounterCache($cache, $created); + } + } + + if (!empty($joined) && $success === true) { + $this->_saveMulti($joined, $this->id, $db); + } + + if ($success && $count > 0) { + if (!empty($this->data)) { + $success = $this->data; + 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 = Hash::merge($success, $this->data); + } + $this->data = false; + $this->_clearCache(); + $this->validationErrors = array(); + } + $this->whitelist = $_whitelist; + return $success; + } + +/** + * Saves model hasAndBelongsToMany data to the database. + * + * @param array $joined Data to save + * @param integer|string $id ID of record in this model + * @param DataSource $db + * @return void + */ + protected function _saveMulti($joined, $id, $db) { + foreach ($joined as $assoc => $data) { + + if (isset($this->hasAndBelongsToMany[$assoc])) { + list($join) = $this->joinModel($this->hasAndBelongsToMany[$assoc]['with']); + + $keyInfo = $this->{$join}->schema($this->{$join}->primaryKey); + if ($with = $this->hasAndBelongsToMany[$assoc]['with']) { + $withModel = is_array($with) ? key($with) : $with; + list($pluginName, $withModel) = pluginSplit($withModel); + $dbMulti = $this->{$withModel}->getDataSource(); + } else { + $dbMulti = $db; + } + + $isUUID = !empty($this->{$join}->primaryKey) && ( + $keyInfo['length'] == 36 && ( + $keyInfo['type'] === 'string' || + $keyInfo['type'] === 'binary' + ) + ); + + $newData = $newValues = $newJoins = array(); + $primaryAdded = false; + + $fields = array( + $dbMulti->name($this->hasAndBelongsToMany[$assoc]['foreignKey']), + $dbMulti->name($this->hasAndBelongsToMany[$assoc]['associationForeignKey']) + ); + + $idField = $db->name($this->{$join}->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[] = String::uuid(); + } + $newValues[$row] = $values; + unset($values); + } elseif (isset($row[$this->hasAndBelongsToMany[$assoc]['associationForeignKey']])) { + if (!empty($row[$this->{$join}->primaryKey])) { + $newJoins[] = $row[$this->hasAndBelongsToMany[$assoc]['associationForeignKey']]; + } + $newData[] = $row; + } elseif (isset($row[$join]) && isset($row[$join][$this->hasAndBelongsToMany[$assoc]['associationForeignKey']])) { + if (!empty($row[$join][$this->{$join}->primaryKey])) { + $newJoins[] = $row[$join][$this->hasAndBelongsToMany[$assoc]['associationForeignKey']]; + } + $newData[] = $row[$join]; + } + } + + $keepExisting = $this->hasAndBelongsToMany[$assoc]['unique'] === 'keepExisting'; + if ($this->hasAndBelongsToMany[$assoc]['unique']) { + $conditions = array( + $join . '.' . $this->hasAndBelongsToMany[$assoc]['foreignKey'] => $id + ); + if (!empty($this->hasAndBelongsToMany[$assoc]['conditions'])) { + $conditions = array_merge($conditions, (array)$this->hasAndBelongsToMany[$assoc]['conditions']); + } + $associationForeignKey = $this->{$join}->alias . '.' . $this->hasAndBelongsToMany[$assoc]['associationForeignKey']; + $links = $this->{$join}->find('all', array( + 'conditions' => $conditions, + 'recursive' => empty($this->hasAndBelongsToMany[$assoc]['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($this->{$join}, $conditions); + } + } + + if (!empty($newData)) { + foreach ($newData as $data) { + $data[$this->hasAndBelongsToMany[$assoc]['foreignKey']] = $id; + if (empty($data[$this->{$join}->primaryKey])) { + $this->{$join}->create(); + } + $this->{$join}->save($data); + } + } + + if (!empty($newValues)) { + if ($keepExisting && !empty($links)) { + foreach ($links as $link) { + $oldJoin = $link[$join][$this->hasAndBelongsToMany[$assoc]['associationForeignKey']]; + if (! in_array($oldJoin, $newJoins) ) { + $conditions[$associationForeignKey] = $oldJoin; + $db->delete($this->{$join}, $conditions); + } else { + unset($newValues[$oldJoin]); + } + } + $newValues = array_values($newValues); + } + if (!empty($newValues)) { + $dbMulti->insertMulti($this->{$join}, $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 boolean $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) { + $keys = empty($keys) ? $this->data[$this->alias] : $keys; + $keys['old'] = isset($keys['old']) ? $keys['old'] : array(); + + foreach ($this->belongsTo as $parent => $assoc) { + if (!empty($assoc['counterCache'])) { + 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 (!$this->{$parent}->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])) { + if ($keys['old'][$foreignKey] != $keys[$foreignKey]) { + $conditions[$fkQuoted] = $keys['old'][$foreignKey]; + $count = intval($this->find('count', compact('conditions', 'recursive'))); + + $this->{$parent}->updateAll( + array($field => $count), + array($this->{$parent}->escapeField() => $keys['old'][$foreignKey]) + ); + } + } + $conditions[$fkQuoted] = $keys[$foreignKey]; + + if ($recursive === 0) { + $conditions = array_merge($conditions, (array)$conditions); + } + $count = intval($this->find('count', compact('conditions', 'recursive'))); + + $this->{$parent}->updateAll( + array($field => $count), + array($this->{$parent}->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 ($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 + * + * @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 http://book.cakephp.org/2.0/en/models/saving-your-data.html#model-saveassociated-array-data-null-array-options-array + * @link http://book.cakephp.org/2.0/en/models/saving-your-data.html#model-saveall-array-data-null-array-options-array + */ + public function saveAll($data, $options = array()) { + $options = array_merge(array('validate' => 'first'), $options); + 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. + * + * @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. + * @link http://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_merge(array('validate' => 'first', 'atomic' => true, 'deep' => false), $options); + $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; + } + + if ($options['atomic']) { + $db = $this->getDataSource(); + $transactionBegun = $db->begin(); + } + $return = array(); + foreach ($data as $key => $record) { + $validates = $this->create(null) !== null; + $saved = false; + if ($validates) { + if ($options['deep']) { + $saved = $this->saveAssociated($record, array_merge($options, array('atomic' => false))); + } else { + $saved = $this->save($record, $options); + } + } + $validates = ($validates && ($saved === true || (is_array($saved) && !in_array(false, $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; + } else { + return true; + } + } + $db->rollback(); + return false; + } + +/** + * 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 boolean True on success, or false on failure. + * @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()) { + 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. + * + * @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. + * @link http://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_merge(array('validate' => 'first', 'atomic' => true, 'deep' => false), $options); + $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, $validates, true))) { + return $validates; + } + $options['validate'] = false; + } + if ($options['atomic']) { + $db = $this->getDataSource(); + $transactionBegun = $db->begin(); + } + + $associations = $this->getAssociated(); + $return = array(); + $validates = true; + foreach ($data as $association => $values) { + $notEmpty = !empty($values[$association]) || (!isset($values[$association]) && !empty($values)); + if (isset($associations[$association]) && $associations[$association] === 'belongsTo' && $notEmpty) { + $validates = $this->{$association}->create(null) !== null; + $saved = false; + if ($validates) { + if ($options['deep']) { + $saved = $this->{$association}->saveAssociated($values, array_merge($options, array('atomic' => false))); + } else { + $saved = $this->{$association}->save($values, array_merge($options, array('atomic' => false))); + } + $validates = ($saved === true || (is_array($saved) && !in_array(false, $saved, true))); + } + if ($validates) { + $key = $this->belongsTo[$association]['foreignKey']; + if (isset($data[$this->alias])) { + $data[$this->alias][$key] = $this->{$association}->id; + } else { + $data = array_merge(array($key => $this->{$association}->id), $data, array($key => $this->{$association}->id)); + } + } else { + $validationErrors[$association] = $this->{$association}->validationErrors; + } + $return[$association] = $validates; + } + } + if ($validates && !($this->create(null) !== null && $this->save($data, $options))) { + $validationErrors[$this->alias] = $this->validationErrors; + $validates = false; + } + $return[$this->alias] = $validates; + + foreach ($data as $association => $values) { + if (!$validates) { + break; + } + $notEmpty = !empty($values[$association]) || (!isset($values[$association]) && !empty($values)); + if (isset($associations[$association]) && $notEmpty) { + $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 = $this->{$association}->create(null) !== null; + $saved = false; + if ($validates) { + if ($options['deep']) { + $saved = $this->{$association}->saveAssociated($values, array_merge($options, array('atomic' => false))); + } else { + $saved = $this->{$association}->save($values, $options); + } + } + $validates = ($validates && ($saved === true || (is_array($saved) && !in_array(false, $saved, true)))); + if (!$validates) { + $validationErrors[$association] = $this->{$association}->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)); + } + } + $_return = $this->{$association}->saveMany($values, array_merge($options, array('atomic' => false))); + if (in_array(false, $_return, true)) { + $validationErrors[$association] = $this->{$association}->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; + } else { + return true; + } + } + $db->rollback(); + return false; + } + +/** + * 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|boolean 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 boolean True on success, false on failure + * @link http://book.cakephp.org/2.0/en/models/saving-your-data.html#model-updateall-array-fields-array-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 integer|string $id ID of record to delete + * @param boolean $cascade Set to true to delete records that depend on this record + * @return boolean True on success + * @link http://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()) { + if (!$this->exists()) { + return false; + } + $db = $this->getDataSource(); + + $this->_deleteDependent($id, $cascade); + $this->_deleteLinks($id); + $this->id = $id; + + $updateCounterCache = false; + if (!empty($this->belongsTo)) { + foreach ($this->belongsTo as $parent => $assoc) { + if (!empty($assoc['counterCache'])) { + $updateCounterCache = true; + break; + } + } + if ($updateCounterCache) { + $keys = $this->find('first', array( + 'fields' => $this->_collectForeignKeys(), + 'conditions' => array($this->alias . '.' . $this->primaryKey => $id), + 'recursive' => -1, + 'callbacks' => false + )); + } + } + + if ($db->delete($this, array($this->alias . '.' . $this->primaryKey => $id))) { + if ($updateCounterCache) { + $this->updateCounterCache($keys[$this->alias]); + } + $this->getEventManager()->dispatch(new CakeEvent('Model.afterDelete', $this)); + $this->_clearCache(); + $this->id = false; + return true; + } + } + return false; + } + +/** + * Cascades model deletes through associated hasMany and hasOne child records. + * + * @param string $id ID of record that was deleted + * @param boolean $cascade Set to true to delete records that depend on this record + * @return void + */ + protected function _deleteDependent($id, $cascade) { + if (!empty($this->__backAssociation)) { + $savedAssociatons = $this->__backAssociation; + $this->__backAssociation = array(); + } + if ($cascade === true) { + foreach (array_merge($this->hasMany, $this->hasOne) as $assoc => $data) { + if ($data['dependent'] === true) { + + $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($savedAssociatons)) { + $this->__backAssociation = $savedAssociatons; + } + } + +/** + * 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 $assoc => $data) { + list($plugin, $joinModel) = pluginSplit($data['with']); + $records = $this->{$joinModel}->find('all', array( + 'conditions' => array($this->{$joinModel}->escapeField($data['foreignKey']) => $id), + 'fields' => $this->{$joinModel}->primaryKey, + 'recursive' => -1, + 'callbacks' => false + )); + if (!empty($records)) { + foreach ($records as $record) { + $this->{$joinModel}->delete($record[$this->{$joinModel}->alias][$this->{$joinModel}->primaryKey]); + } + } + } + } + +/** + * Deletes multiple model records based on a set of conditions. + * + * @param mixed $conditions Conditions to match + * @param boolean $cascade Set to true to delete records that depend on this record + * @param boolean $callbacks Run callbacks + * @return boolean True on success, false on failure + * @link http://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); + } else { + $ids = $this->find('all', array_merge(array( + 'fields' => "{$this->alias}.{$this->primaryKey}", + 'recursive' => 0), compact('conditions')) + ); + if ($ids === false) { + 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; + } else { + 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 + * @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 integer|string $id ID of record to check for existence + * @return boolean True if such a record exists + */ + public function exists($id = null) { + if ($id === null) { + $id = $this->getID(); + } + if ($id === false) { + return false; + } + $conditions = array($this->alias . '.' . $this->primaryKey => $id); + $query = array('conditions' => $conditions, 'recursive' => -1, 'callbacks' => false); + return ($this->find('count', $query) > 0); + } + +/** + * Returns true if a record that meets given conditions exists. + * + * @param array $conditions SQL conditions array + * @return boolean True if such a record exists + */ + public function hasAny($conditions = null) { + return ($this->find('count', array('conditions' => $conditions, 'recursive' => -1)) != false); + } + +/** + * Queries the datasource and returns a result set array. + * + * Also used to perform notation finds, 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') + * + * Eg: + * {{{ + * find('all', array( + * 'conditions' => array('name' => 'Thomas Anderson'), + * 'fields' => array('name', 'email'), + * 'order' => 'field3 DESC', + * 'recursive' => 2, + * 'group' => 'type' + * )); + * }}} + * + * 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. + * + * {{{ + * find('all', array( + * 'conditions' => array('name' => 'Thomas Anderson'), + * 'joins' => array( + * array( + * 'alias' => 'Thought', + * 'table' => 'thoughts', + * 'type' => 'LEFT', + * 'conditions' => '`Thought`.`person_id` = `Person`.`id`' + * ) + * ) + * )); + * }}} + * + * Behaviors and find types can also define custom finder keys which are passed into find(). + * + * 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. + * @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 Array of records + * @link http://book.cakephp.org/2.0/en/models/deleting-data.html#deleteall + */ + public function find($type = 'first', $query = array()) { + $this->findQueryType = $type; + $this->id = $this->getID(); + + $query = $this->buildQuery($type, $query); + if (is_null($query)) { + return null; + } + + $results = $this->getDataSource()->read($this, $query); + $this->resetAssociations(); + + if ($query['callbacks'] === true || $query['callbacks'] === 'after') { + $results = $this->_filterResults($results); + } + + $this->findQueryType = null; + + if ($type === 'all') { + return $results; + } else { + 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 Query array or null if it could not be build for some reasons + * @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 ($type !== 'all') { + if ($this->findMethods[$type] === true) { + $query = $this->{'_find' . ucfirst($type)}('before', $query); + } + } + + if (!is_numeric($query['page']) || intval($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; + } + $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('first') operations. Only called by Model::find(). + * + * @param string $state Either "before" or "after" + * @param array $query + * @param array $results + * @return array + * @see Model::find() + */ + protected function _findFirst($state, $query, $results = array()) { + if ($state === 'before') { + $query['limit'] = 1; + return $query; + } elseif ($state === 'after') { + if (empty($results[0])) { + return false; + } + 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 + * @param array $results + * @return integer 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; + } elseif ($state === 'after') { + foreach (array(0, $this->alias) as $key) { + if (isset($results[0][$key]['count'])) { + if (($count = count($results)) > 1) { + return $count; + } else { + return intval($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 + * @param array $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'] = String::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; + } elseif ($state === 'after') { + if (empty($results)) { + return array(); + } + $lst = $query['list']; + return Hash::combine($results, $lst['keyPath'], $lst['valuePath'], $lst['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 + * @param array $results + * @return array + */ + protected function _findNeighbors($state, $query, $results = array()) { + if ($state === 'before') { + extract($query); + $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; + } elseif ($state === 'after') { + extract($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'] = $neighbors[0]; + } + 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 + * @param mixed $query + * @param array $results + * @return array Threaded results + */ + protected function _findThreaded($state, $query, $results = array()) { + if ($state === 'before') { + return $query; + } elseif ($state === 'after') { + $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 afterFilter() methods. + * + * @param array $results Results to filter + * @param boolean $primary If this is the primary model results (results from model where the find operation was performed) + * @return array Set of filtered results + */ + 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 boolean 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. + * + * @param array $fields Field/value pairs to search (if no values specified, they are pulled from $this->data) + * @param boolean $or If false, all fields specified must match in order for a false return value + * @return boolean False if any records matching any fields are found + */ + public function isUnique($fields, $or = true) { + if (!is_array($fields)) { + $fields = func_get_args(); + if (is_bool($fields[count($fields) - 1])) { + $or = $fields[count($fields) - 1]; + unset($fields[count($fields) - 1]); + } + } + + foreach ($fields as $field => $value) { + if (is_numeric($field)) { + unset($fields[$field]); + + $field = $value; + if (isset($this->data[$this->alias][$field])) { + $value = $this->data[$this->alias][$field]; + } else { + $value = null; + } + } + + 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)) == 0); + } + +/** + * Returns a resultset for a given SQL statement. Custom SQL queries should be performed with this method. + * + * @param string $sql,... SQL statement + * @return mixed Resultset array or boolean indicating success / failure depending on the query executed + * @link http://book.cakephp.org/2.0/en/models/retrieving-your-data.html#model-query + */ + public function query($sql) { + $params = func_get_args(); + $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 boolean True if there are no errors + */ + public function validates($options = array()) { + return $this->validator()->validates($options); + } + +/** + * Returns an array of fields that have failed validation. On the current model. + * + * @param string $options An optional array of custom options to be made available in the beforeValidate callback + * @return array Array of invalid fields + * @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 boolean 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 $assoc => $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 integer $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]; + } elseif (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 integer|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 integer Number of rows + */ + public function getNumRows() { + return $this->getDataSource()->lastNumRows(); + } + +/** + * Returns the number of rows affected by the last query. + * + * @return integer 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 != null) { + $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']; + } + + $this->schemaName = $db->getSchemaName(); + } + +/** + * 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 Associations + */ + public function getAssociated($type = null) { + if ($type == null) { + $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; + } elseif (in_array($type, $this->_associations)) { + if (empty($this->{$type})) { + return array(); + } + return array_keys($this->{$type}); + } else { + $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())); + } elseif (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', $model->alias), + 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 $queryData 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 + * $queryData to continue with new $queryData + * @link http://book.cakephp.org/2.0/en/models/callback-methods.html#beforefind + */ + public function beforeFind($queryData) { + 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 boolean $primary Whether this model is being queried directly (vs. being queried as an association) + * @return mixed Result of the find operation + * @link http://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 + * @return boolean True if the operation should continue, false if it should abort + * @link http://book.cakephp.org/2.0/en/models/callback-methods.html#beforesave + */ + public function beforeSave($options = array()) { + return true; + } + +/** + * Called after each successful save operation. + * + * @param boolean $created True if this save created a new record + * @return void + * @link http://book.cakephp.org/2.0/en/models/callback-methods.html#aftersave + */ + public function afterSave($created) { + } + +/** + * Called before every deletion operation. + * + * @param boolean $cascade If true records that depend on this record will also be deleted + * @return boolean True if the operation should continue, false if it should abort + * @link http://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 http://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(), see $options of model::save(). + * @return boolean True if validate operation should continue, false to abort + * @link http://book.cakephp.org/2.0/en/models/callback-methods.html#beforevalidate + */ + 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 http://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 boolean true on delete + */ + protected function _clearCache($type = null) { + if ($type === null) { + if (Configure::read('Cache.check') === true) { + $assoc[] = strtolower(Inflector::pluralize($this->alias)); + $assoc[] = strtolower(Inflector::underscore(Inflector::pluralize($this->alias))); + foreach ($this->_associations as $key => $association) { + foreach ($this->$association as $key => $className) { + $check = strtolower(Inflector::pluralize($className['className'])); + if (!in_array($check, $assoc)) { + $assoc[] = strtolower(Inflector::pluralize($className['className'])); + $assoc[] = strtolower(Inflector::underscore(Inflector::pluralize($className['className']))); + } + } + } + clearCache($assoc); + return true; + } + } else { + //Will use for query cache deleting + } + } + +/** + * Retunrs an instance of a model validator for this class + * + * @return ModelValidator + */ + public function validator($instance = null) { + if ($instance instanceof ModelValidator) { + return $this->_validator = $instance; + } + + if (empty($this->_validator) && is_null($instance)) { + $this->_validator = new ModelValidator($this); + } + + return $this->_validator; + } + +} diff --git a/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/ModelBehavior.php b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/ModelBehavior.php new file mode 100644 index 0000000..141b9d5 --- /dev/null +++ b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/ModelBehavior.php @@ -0,0 +1,236 @@ +Model->doSomething($arg1, $arg2);`. + * + * ### Mapped methods + * + * Behaviors can also define mapped methods. Mapped methods use pattern matching for method invocation. This + * allows you to create methods similar to Model::findAllByXXX methods on your behaviors. Mapped methods need to + * be declared in your behaviors `$mapMethods` array. The method signature for a mapped method is slightly different + * than a normal behavior mixin method. + * + * {{{ + * public $mapMethods = array('/do(\w+)/' => 'doSomething'); + * + * function doSomething(Model $model, $method, $arg1, $arg2) { + * //do something + * } + * }}} + * + * The above will map every doXXX() method call to the behavior. As you can see, the model is + * still the first parameter, but the called method name will be the 2nd parameter. This allows + * you to munge the method name for additional information, much like Model::findAllByXX. + * + * @package Cake.Model + * @see Model::$actsAs + * @see BehaviorCollection::load() + */ +class ModelBehavior extends Object { + +/** + * 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 boolean|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 boolean $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) { + } + +/** + * 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 + * @return mixed False or null will abort the operation. Any other result will continue. + */ + public function beforeValidate(Model $model) { + 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 + * @return mixed False if the operation should abort. Any other result will continue. + */ + public function beforeSave(Model $model) { + return true; + } + +/** + * afterSave is called after a model is saved. + * + * @param Model $model Model using this behavior + * @param boolean $created True if this save created a new record + * @return boolean + */ + public function afterSave(Model $model, $created) { + 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 boolean $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/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/ModelValidator.php b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/ModelValidator.php new file mode 100644 index 0000000..dbe58ce --- /dev/null +++ b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/ModelValidator.php @@ -0,0 +1,599 @@ +_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 boolean 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|boolean 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_merge(array('atomic' => true, 'deep' => false), $options); + $model->validationErrors = $validationErrors = $return = array(); + $model->create(null); + if (!($model->set($data) && $model->validates($options))) { + $validationErrors[$model->alias] = $model->validationErrors; + $return[$model->alias] = false; + } else { + $return[$model->alias] = true; + } + $data = $model->data; + if (!empty($options['deep']) && isset($data[$model->alias])) { + $recordData = $data[$model->alias]; + unset($data[$model->alias]); + $data = array_merge($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)) { + if (in_array(false, $validates, true)) { + $validates = false; + } else { + $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 boolean True on success, or false on failure. + * @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_merge(array('atomic' => true, 'deep' => false), $options); + $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, $validates, true))) { + $validationErrors[$key] = $model->validationErrors; + $validates = false; + } else { + $validates = true; + } + $return[$key] = $validates; + } + $model->validationErrors = $validationErrors; + if (!$options['atomic']) { + return $return; + } + if (empty($model->validationErrors)) { + return true; + } + return false; + } + +/** + * 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 Array of invalid fields + * @see ModelValidator::validates() + */ + public function errors($options = array()) { + if (!$this->_triggerBeforeValidate($options)) { + return false; + } + $model = $this->getModel(); + + if (!$this->_parseRules()) { + return $model->validationErrors; + } + + $fieldList = isset($options['fieldList']) ? $options['fieldList'] : array(); + $exists = $model->exists(); + $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 $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() { + if (!empty($this->_methods)) { + return $this->_methods; + } + + $methods = array(); + foreach (get_class_methods($this->_model) as $method) { + $methods[strtolower($method)] = array($this->_model, $method); + } + + 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 + */ + public function getField($name = null) { + $this->_parseRules(); + if ($name !== null && !empty($this->_fields[$name])) { + return $this->_fields[$name]; + } elseif ($name !== null) { + 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 boolean 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 ModelValidator + */ + 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 Model's whitelist or passed fieldList and returns the list of fields + * to be validated + * + * @param array $fieldList list of fields to be used for validation + * @return array List of validation rules to be applied + */ + protected function _validationList($fieldList = array()) { + $model = $this->getModel(); + $whitelist = $model->whitelist; + + if (!empty($fieldList)) { + if (!empty($fieldList[$model->alias]) && is_array($fieldList[$model->alias])) { + $whitelist = $fieldList[$model->alias]; + } else { + $whitelist = $fieldList; + } + } + unset($fieldList); + + $validateList = array(); + if (!empty($whitelist)) { + $this->validationErrors = array(); + + foreach ((array)$whitelist as $f) { + if (!empty($this->_fields[$f])) { + $validateList[$f] = $this->_fields[$f]; + } + } + } else { + return $this->_fields; + } + + 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 boolean 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]; + } + } + if (empty($newData)) { + continue; + } + 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 + * @return boolean + */ + 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 wheter a rule set is defined for a field or not + * + * @param string $field name of the field to check + * @return boolean + **/ + 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 rulset 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 argumet 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' => 'notEmpty', 'required' => true)) + * ->add('user_id', 'valid', array('rule' => 'numeric', 'message' => 'Invalid User')) + * + * $validator->add('password', array( + * 'size' => array('rule' => array('between', 8, 20)), + * 'hasSpecialCharacter' => array('rule' => 'validateSpecialchar', 'message' => 'not valid') + * )); + * }}} + * + * @param string $field The name of the field from wich the rule will be removed + * @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 ModelValidator this instance + **/ + 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 wich the rule will be removed + * @param string $rule the name of the rule to be removed + * @return ModelValidator this instance + **/ + public function remove($field, $rule = null) { + $this->_parseRules(); + if ($rule === null) { + unset($this->_fields[$field]); + } else { + $this->_fields[$field]->removeRule($rule); + } + return $this; + } +} diff --git a/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Permission.php b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Permission.php new file mode 100644 index 0000000..8decc7f --- /dev/null +++ b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Permission.php @@ -0,0 +1,257 @@ +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 boolean Success (true if ARO has access to action in ACO, false otherwise) + */ + public function check($aro, $aco, $action = "*") { + if ($aro == null || $aco == null) { + return false; + } + + $permKeys = $this->getAcoKeys($this->schema()); + $aroPath = $this->Aro->node($aro); + $acoPath = $this->Aco->node($aco); + + if (empty($aroPath) || empty($acoPath)) { + trigger_error(__d('cake_dev', "DbAcl::check() - Failed ARO/ACO node lookup in permissions check. Node references:\nAro: ") . print_r($aro, true) . "\nAco: " . print_r($aco, true), E_USER_WARNING); + return false; + } + + if ($acoPath == null || $acoPath == array()) { + trigger_error(__d('cake_dev', "DbAcl::check() - Failed ACO node lookup in permissions check. Node references:\nAro: ") . print_r($aro, true) . "\nAco: " . print_r($aco, true), E_USER_WARNING); + return false; + } + + if ($action != '*' && !in_array('_' . $action, $permKeys)) { + trigger_error(__d('cake_dev', "ACO permissions key %s does not exist in DbAcl::check()", $action), E_USER_NOTICE); + return false; + } + + $inherited = array(); + $acoIDs = Hash::extract($acoPath, '{n}.' . $this->Aco->alias . '.id'); + + $count = count($aroPath); + 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 + )); + + if (empty($perms)) { + continue; + } else { + $perms = Hash::extract($perms, '{n}.' . $this->alias); + foreach ($perms as $perm) { + if ($action == '*') { + + foreach ($permKeys as $key) { + if (!empty($perm)) { + if ($perm[$key] == -1) { + return false; + } elseif ($perm[$key] == 1) { + $inherited[$key] = 1; + } + } + } + + if (count($inherited) === count($permKeys)) { + return true; + } + } else { + switch ($perm['_' . $action]) { + case -1: + return false; + case 0: + continue; + break; + case 1: + return true; + break; + } + } + } + } + } + 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 *) + * @param integer $value Value to indicate access type (1 to give access, -1 to deny, 0 to inherit) + * @return boolean Success + */ + public function allow($aro, $aco, $actions = "*", $value = 1) { + $perms = $this->getAclLink($aro, $aco); + $permKeys = $this->getAcoKeys($this->schema()); + $save = array(); + + if ($perms == false) { + trigger_error(__d('cake_dev', 'DbAcl::allow() - Invalid node'), E_USER_WARNING); + return false; + } + if (isset($perms[0])) { + $save = $perms[0][$this->alias]; + } + + if ($actions == "*") { + $save = array_combine($permKeys, array_pad(array(), count($permKeys), $value)); + } else { + if (!is_array($actions)) { + $actions = array('_' . $actions); + } + if (is_array($actions)) { + foreach ($actions as $action) { + if ($action{0} != '_') { + $action = '_' . $action; + } + if (in_array($action, $permKeys)) { + $save[$action] = $value; + } + } + } + } + list($save['aro_id'], $save['aco_id']) = array($perms['aro'], $perms['aco']); + + if ($perms['link'] != null && !empty($perms['link'])) { + $save['id'] = $perms['link'][0][$this->alias]['id']; + } else { + unset($save['id']); + $this->id = null; + } + return ($this->save($save) !== false); + } + +/** + * 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 (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); + + return array( + 'aro' => $aro, + 'aco' => $aco, + 'link' => $this->find('all', array('conditions' => array( + $this->alias . '.aro_id' => $aro, + $this->alias . '.aco_id' => $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; + } +} diff --git a/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Validator/CakeValidationRule.php b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Validator/CakeValidationRule.php new file mode 100644 index 0000000..8ebf825 --- /dev/null +++ b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Validator/CakeValidationRule.php @@ -0,0 +1,333 @@ +_addValidatorProps($validator); + } + +/** + * Checks if the rule is valid + * + * @return boolean + */ + 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 boolean + */ + public function isEmptyAllowed() { + return $this->skip() || $this->allowEmpty === true; + } + +/** + * Checks if the field is required according to the `required` property + * + * @return boolean + */ + 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; + } else { + return false; + } + } + + return $this->required; + } + +/** + * Checks whether the field failed the `field should be present` validation + * + * @param array $data data to check rule against + * @return boolean + */ + public function checkRequired($field, &$data) { + return ( + (!isset($data[$field]) && $this->isRequired() === true) || + ( + isset($data[$field]) && (empty($data[$field]) && + !is_numeric($data[$field])) && $this->allowEmpty === false + ) + ); + } + +/** + * Checks if the allowEmpty key applies + * + * @param array $data data to check rule against + * @return boolean + */ + 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 boolean 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 whethere this rule should break validation process for associated field + * after it fails + * + * @return boolean + */ + 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 wheter 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. + * + * @return boolean + **/ + public function isUpdate($exists = null) { + if ($exists === null) { + return $this->_recordExists; + } + return $this->_recordExists = $exists; + } + +/** + * Dispatches the validation rule to the given validator method + * + * @return boolean 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]); + } elseif (Configure::read('debug') > 0) { + trigger_error(__d('cake_dev', 'Could not find validation handler %s for %s', $this->_rule, $field), E_USER_WARNING); + return false; + } + + return true; + } + +/** + * Returns passed options for this rule + * + * @return array + **/ + 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 + * + * @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]); + } + } + +} diff --git a/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Validator/CakeValidationSet.php b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Validator/CakeValidationSet.php new file mode 100644 index 0000000..fdecdc1 --- /dev/null +++ b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Validator/CakeValidationSet.php @@ -0,0 +1,350 @@ +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 + * + * @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 + * + * @return array list of validation errors for this field + */ + public function validate($data, $isUpdate = false) { + $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; + } + +/** + * Gets a rule for a given name if exists + * + * @param string $name + * @return CakeValidationRule + */ + public function getRule($name) { + if (!empty($this->_rules[$name])) { + return $this->_rules[$name]; + } + } + +/** + * Returns all rules for this validation set + * + * @return array + */ + public function getRules() { + return $this->_rules; + } + +/** + * Sets a CakeValidationRule $rule with a $name + * + * ## Example: + * + * {{{ + * $set + * ->setRule('required', array('rule' => 'notEmpty', 'required' => true)) + * ->setRule('inRange', array('rule' => array('between', 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 CakeValidationSet this instance + */ + 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 CakeValidationSet this instance + */ + public function removeRule($name) { + unset($this->_rules[$name]); + return $this; + } + +/** + * Sets the rules for a given field + * + * ## Example: + * + * {{{ + * $set->setRules(array( + * 'required' => array('rule' => 'notEmpty', 'required' => true), + * 'inRange' => array('rule' => array('between', 4, 10) + * )); + * }}} + * + * @param array $rules The rules to be set + * @param bolean $mergeVars [optional] If true, merges vars instead of replace. Defaults to true. + * @return ModelField + */ + public function setRules($rules = array(), $mergeVars = true) { + if ($mergeVars === false) { + $this->_rules = $rules; + } else { + $this->_rules = array_merge($this->_rules, $rules); + } + 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_dev', '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 wheter an index exists in the rule set + * + * @param string $index name of the rule + * @return boolean + **/ + 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 + * + * @param string $index name of the rule + * @param CakeValidationRule|array rule to add to $index + **/ + 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); + } + +} -- cgit v1.2.3