MyModel.php 49 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718
  1. <?php
  2. App::uses('Model', 'Model');
  3. App::uses('Utility', 'Tools.Utility');
  4. /**
  5. * Model enhancements for Cake2
  6. *
  7. * @author Mark Scherer
  8. * @license MIT
  9. * 2012-02-27 ms
  10. */
  11. class MyModel extends Model {
  12. public $recursive = -1;
  13. public $actsAs = array('Containable');
  14. /**
  15. * MyModel::__construct()
  16. *
  17. * @param integer $id
  18. * @param string $table
  19. * @param string $ds
  20. * @return void
  21. */
  22. public function __construct($id = false, $table = null, $ds = null) {
  23. parent::__construct($id, $table, $ds);
  24. # enable caching
  25. if (!Configure::read('Cache.disable') && Cache::config('sql') === false) {
  26. if (!file_exists(CACHE . 'sql')) {
  27. mkdir(CACHE . 'sql', CHOWN_PUBLIC);
  28. }
  29. Cache::config('sql', array(
  30. 'engine' => 'File',
  31. 'serialize' => true,
  32. 'prefix' => '',
  33. 'path' => CACHE .'sql'. DS,
  34. 'duration' => '+1 day'
  35. ));
  36. }
  37. if (!Configure::read('Model.disablePrefixing')) {
  38. $this->prefixOrderProperty();
  39. }
  40. # get a notice if there is an AppModel instances instead of real Models (in those cases usually a dev error!)
  41. if (defined('HTTP_HOST') && HTTP_HOST && !is_a($this, $this->name) && $this->displayField !== 'id' && $this->useDbConfig !== 'test' && !Configure::read('Core.disableModelInstanceNotice')) {
  42. trigger_error('AppModel instance! Expected: ' . $this->name);
  43. }
  44. }
  45. /**
  46. * Prefixes the order property with the actual alias if its a string or array.
  47. *
  48. * The core fails on using the proper prefix when building the query with two
  49. * different tables.
  50. *
  51. * @return void
  52. */
  53. public function prefixOrderProperty() {
  54. if (is_string($this->order)) {
  55. $this->order = $this->prefixAlias($this->order);
  56. }
  57. if (is_array($this->order)) {
  58. foreach ($this->order as $key => $value) {
  59. if (is_numeric($key)) {
  60. $this->order[$key] = $this->prefixAlias($value);
  61. } else {
  62. $this->order[$this->prefixAlias($key)] = $value;
  63. }
  64. }
  65. }
  66. }
  67. /**
  68. * Checks if a string of a field name contains a dot if not it will add it and add the alias prefix.
  69. *
  70. * @param string
  71. * @return string
  72. */
  73. public function prefixAlias($string) {
  74. if (strpos($string, '.') === false) {
  75. return $this->alias . '.' . $string;
  76. }
  77. return $string;
  78. }
  79. /**
  80. * Deconstructs a complex data type (array or object) into a single field value.
  81. * BUGFIXED VERSION - autodetects type and allows manual override
  82. *
  83. * @param string $field The name of the field to be deconstructed
  84. * @param array|object $data An array or object to be deconstructed into a field
  85. * @return mixed The resulting data that should be assigned to a field
  86. */
  87. public function deconstruct($field, $data, $type = null) {
  88. if (!is_array($data)) {
  89. return $data;
  90. }
  91. if ($type === null) {
  92. $type = $this->getColumnType($field);
  93. }
  94. if ($type === null) {
  95. //try to autodetect
  96. if (isset($data['day']) || isset($data['month']) || isset($data['year'])) {
  97. $type = 'date';
  98. }
  99. if (isset($data['hour']) || isset($data['min']) || isset($data['sec'])) {
  100. $type .= 'time';
  101. }
  102. }
  103. if (in_array($type, array('datetime', 'timestamp', 'date', 'time'))) {
  104. $useNewDate = (isset($data['year']) || isset($data['month']) ||
  105. isset($data['day']) || isset($data['hour']) || isset($data['minute']));
  106. $dateFields = array('Y' => 'year', 'm' => 'month', 'd' => 'day', 'H' => 'hour', 'i' => 'min', 's' => 'sec');
  107. $timeFields = array('H' => 'hour', 'i' => 'min', 's' => 'sec');
  108. $date = array();
  109. if (isset($data['meridian']) && empty($data['meridian'])) {
  110. return null;
  111. }
  112. if (
  113. isset($data['hour']) &&
  114. isset($data['meridian']) &&
  115. !empty($data['hour']) &&
  116. $data['hour'] != 12 &&
  117. 'pm' == $data['meridian']
  118. ) {
  119. $data['hour'] = $data['hour'] + 12;
  120. }
  121. if (isset($data['hour']) && isset($data['meridian']) && $data['hour'] == 12 && 'am' == $data['meridian']) {
  122. $data['hour'] = '00';
  123. }
  124. if ($type === 'time') {
  125. foreach ($timeFields as $key => $val) {
  126. if (!isset($data[$val]) || $data[$val] === '0' || $data[$val] === '00') {
  127. $data[$val] = '00';
  128. } elseif ($data[$val] !== '') {
  129. $data[$val] = sprintf('%02d', $data[$val]);
  130. }
  131. if (!empty($data[$val])) {
  132. $date[$key] = $data[$val];
  133. } else {
  134. return null;
  135. }
  136. }
  137. }
  138. if ($type === 'datetime' || $type === 'timestamp' || $type === 'date') {
  139. foreach ($dateFields as $key => $val) {
  140. if ($val === 'hour' || $val === 'min' || $val === 'sec') {
  141. if (!isset($data[$val]) || $data[$val] === '0' || $data[$val] === '00') {
  142. $data[$val] = '00';
  143. } else {
  144. $data[$val] = sprintf('%02d', $data[$val]);
  145. }
  146. }
  147. if (!isset($data[$val]) || isset($data[$val]) && (empty($data[$val]) || $data[$val][0] === '-')) {
  148. return null;
  149. }
  150. if (isset($data[$val]) && !empty($data[$val])) {
  151. $date[$key] = $data[$val];
  152. }
  153. }
  154. }
  155. if ($useNewDate && !empty($date)) {
  156. $format = $this->getDataSource()->columns[$type]['format'];
  157. foreach (array('m', 'd', 'H', 'i', 's') as $index) {
  158. if (isset($date[$index])) {
  159. $date[$index] = sprintf('%02d', $date[$index]);
  160. }
  161. }
  162. return str_replace(array_keys($date), array_values($date), $format);
  163. }
  164. }
  165. return $data;
  166. }
  167. /**
  168. * The main method for any enumeration, should be called statically
  169. * Now also supports reordering/filtering
  170. *
  171. * @link http://www.dereuromark.de/2010/06/24/static-enums-or-semihardcoded-attributes/
  172. * @param string $value or array $keys or NULL for complete array result
  173. * @param array $options (actual data)
  174. * @return mixed string/array
  175. * static enums
  176. * 2009-11-05 ms
  177. */
  178. public static function enum($value, $options, $default = null) {
  179. if ($value !== null && !is_array($value)) {
  180. if (array_key_exists($value, $options)) {
  181. return $options[$value];
  182. }
  183. return $default;
  184. } elseif ($value !== null) {
  185. $newOptions = array();
  186. foreach ($value as $v) {
  187. $newOptions[$v] = $options[$v];
  188. }
  189. return $newOptions;
  190. }
  191. return $options;
  192. }
  193. /**
  194. * Catch database errors before it's too late
  195. * //TODO: testing
  196. * 2010-11-04 ms
  197. */
  198. public function onError() {
  199. $err = $this->lastError();
  200. if (!empty($err)) {
  201. $this->log($err, 'sql_error');
  202. } else {
  203. $this->log('unknown error', 'sql_error');
  204. }
  205. if (!empty($this->data)) {
  206. $data = $this->data;
  207. } elseif ($this->id) {
  208. $data = 'id ' . $this->id;
  209. } else {
  210. $data = 'no data';
  211. }
  212. $data .= ' (' . env('REDIRECT_URL') . ')';
  213. $this->log($data, 'sql_error');
  214. }
  215. /**
  216. * @return string Error message with error number
  217. * 2010-11-06 ms
  218. */
  219. public function lastError() {
  220. $db = $this->getDataSource();
  221. return $db->lastError();
  222. }
  223. /**
  224. * combine virtual fields with fields values of find()
  225. * USAGE:
  226. * $this->Model->find('all', array('fields' => $this->Model->virtualFields('full_name')));
  227. * Also adds the field to the virtualFields array of the model (for correct result)
  228. * TODO: adding of fields only temperory!
  229. * @param array $virtualFields to include
  230. * 2011-10-13 ms
  231. */
  232. public function virtualFields($fields = array()) {
  233. $res = array();
  234. foreach ((array)$fields as $field => $sql) {
  235. if (is_int($field)) {
  236. $field = $sql;
  237. $sql = null;
  238. }
  239. $plugin = $model = null;
  240. if (($pos = strrpos($field, '.')) !== false) {
  241. $model = substr($field, 0, $pos);
  242. $field = substr($field, $pos+1);
  243. if (($pos = strrpos($model, '.')) !== false) {
  244. list($plugin, $model) = pluginSplit($model);
  245. }
  246. }
  247. if (empty($model)) {
  248. $model = $this->alias;
  249. if ($sql === null) {
  250. $sql = $this->virtualFields[$field];
  251. } else {
  252. $this->virtualFields[$field] = $sql;
  253. }
  254. } else {
  255. if (!isset($this->$model)) {
  256. $fullModelName = ($plugin ? $plugin.'.' : '') . $model;
  257. $this->$model = ClassRegistry::init($fullModelName);
  258. }
  259. if ($sql === null) {
  260. $sql = $this->$model->virtualFields[$field];
  261. } else {
  262. $this->$model->virtualFields[$field] = $sql;
  263. }
  264. }
  265. $res[] = $sql . ' AS ' . $model . '__' . $field;
  266. }
  267. return $res;
  268. }
  269. /**
  270. * HIGHLY EXPERIMENTAL
  271. * manually escape value for updateAll() etc
  272. * 2011-06-27 ms
  273. */
  274. public function escapeValue($value) {
  275. if ($value === null || is_numeric($value)) {
  276. return $value;
  277. }
  278. if (is_bool($value)) {
  279. return (int)$value;
  280. }
  281. return "'" . $value . "'";
  282. }
  283. /**
  284. * HIGHLY EXPERIMENTAL
  285. * @see http://cakephp.lighthouseapp.com/projects/42648/tickets/1799-model-should-have-escapefield-method
  286. * 2011-07-05 ms
  287. */
  288. public function value($content) {
  289. $db = $this->getDatasource();
  290. return $db->value($content);
  291. }
  292. /**
  293. * TODO: move to behavior (Incremental)
  294. * @param mixed id (single string)
  295. * @param options:
  296. * - step (defaults to 1)
  297. * - current (if none it will get it from db)
  298. * - reset (if true, it will be set to 0)
  299. * - field (defaults to 'count')
  300. * - modify (if true if will affect modified timestamp)
  301. * - timestampField (if provided it will be filled with NOW())
  302. * 2010-06-08 ms
  303. */
  304. public function up($id, $customOptions = array()) {
  305. $step = 1;
  306. if (isset($customOptions['step'])) {
  307. $step = $customOptions['step'];
  308. }
  309. $field = 'count';
  310. if (isset($customOptions['field'])) {
  311. $field = $customOptions['field'];
  312. }
  313. if (isset($customOptions['reset'])) {
  314. $currentValue = $step = 0;
  315. } elseif (!isset($customOptions['current'])) {
  316. $currentValue = $this->field($field, array($this->alias.'.id'=>$id));
  317. if ($currentValue === false) {
  318. return false;
  319. }
  320. } else {
  321. $currentValue = $customOptions['current'];
  322. }
  323. $value = (int)$currentValue + (int)$step;
  324. $data = array($field=>$value);
  325. if (empty($customOptions['modify'])) {
  326. $data['modified'] = false;
  327. }
  328. if (!empty($customOptions['timestampField'])) {
  329. $data[$customOptions['timestampField']] = date(FORMAT_DB_DATETIME);
  330. }
  331. $this->id = $id;
  332. return $this->save($data, false);
  333. }
  334. /**
  335. * return the next auto increment id from the current table
  336. * UUIDs will return false
  337. *
  338. * @return integer next auto increment value or False on failure
  339. */
  340. public function getNextAutoIncrement() {
  341. $next_increment = 0;
  342. $query = "SHOW TABLE STATUS WHERE name = '" . $this->tablePrefix . $this->table . "'";
  343. $result = $this->query($query);
  344. if (!isset($result[0]['TABLES']['Auto_increment'])) {
  345. return false;
  346. }
  347. return (int)$result[0]['TABLES']['Auto_increment'];
  348. }
  349. /**
  350. * workaround for a cake bug which sets empty fields to NULL in Model::set()
  351. * we cannot use if (isset() && empty()) statements without this fix
  352. * @param array $fields (which are supposed to be present in $this->data[$this->alias])
  353. * @param boolean $force (if init should be forced, otherwise only if array_key exists)
  354. * 2011-03-06 ms
  355. */
  356. public function init($fields = array(), $force = false) {
  357. foreach ($fields as $field) {
  358. if ($force || array_key_exists($field, $this->data[$this->alias])) {
  359. if (!isset($this->data[$this->alias][$field])) {
  360. $this->data[$this->alias][$field] = '';
  361. }
  362. }
  363. }
  364. }
  365. /**
  366. * Fix for non atomic queries (MyISAM etc) and saveAll to still return just the boolean result
  367. * Otherwise you would have to iterate over all result values to find out if the save was successful.
  368. *
  369. * Use Configure::read('Model.atomic') to modify atomic behavior.
  370. * Additional options:
  371. *
  372. * - returnArray: bool
  373. *
  374. * @param mixed $data
  375. * @param array $options
  376. * @return boolean Success
  377. * 2012-11-10 ms
  378. */
  379. public function saveAll($data = null, $options = array()) {
  380. if (!isset($options['atomic']) && Configure::read('Model.atomic') !== null) {
  381. $options['atomic'] = (bool)Configure::read('Model.atomic');
  382. }
  383. $res = parent::saveAll($data, $options);
  384. if (is_array($res) && empty($options['returnArray'])) {
  385. $res = Utility::isValidSaveAll($res);
  386. }
  387. return $res;
  388. }
  389. /**
  390. * enables HABTM-Validation
  391. * e.g. with
  392. * 'rule' => array('multiple', array('min' => 2))
  393. * 2010-01-14 ms
  394. */
  395. public function beforeValidate($options = array()) {
  396. foreach ($this->hasAndBelongsToMany as $k => $v) {
  397. if (isset($this->data[$k][$k])) {
  398. $this->data[$this->alias][$k] = $this->data[$k][$k];
  399. }
  400. }
  401. return parent::beforeValidate($options);
  402. }
  403. /**
  404. * @param params
  405. * - key: functioName or other key used
  406. * @return boolean Success
  407. * 2010-12-02 ms
  408. */
  409. public function deleteCache($key) {
  410. $key = Inflector::underscore($key);
  411. if (!empty($key)) {
  412. return Cache::delete(strtolower(Inflector::underscore($this->alias)) . '__' . $key, 'sql');
  413. }
  414. return Cache::clear(false, 'sql');
  415. }
  416. /**
  417. * Generates a SQL subquery snippet to be used in your actual query.
  418. * Your subquery snippet needs to return a single value or flat array of values.
  419. *
  420. * Example:
  421. *
  422. * $this->Model->find('first', array(
  423. * 'conditions' => array('NOT' => array('some_id' => $this->Model->subquery(...)))
  424. * ))
  425. *
  426. * Note: You might have to set `autoFields` to false in order to retrieve only the fields you request:
  427. * http://book.cakephp.org/2.0/en/core-libraries/behaviors/containable.html#containablebehavior-options
  428. *
  429. * @param string $type The type of the query ('count'/'all'/'first' - first only works with some mysql versions)
  430. * @param array $options The options array
  431. * @param string $alias You can use this intead of $options['alias'] if you want
  432. * @param boolean $parenthesise Add parenthesis before and after
  433. * @return string result sql snippet of the query to run
  434. * @modified Mark Scherer (cake2.x ready and improvements)
  435. * @link http://bakery.cakephp.org/articles/lucaswxp/2011/02/11/easy_and_simple_subquery_cakephp
  436. * 2011-07-05 ms
  437. */
  438. public function subquery($type, $options = array(), $alias = null, $parenthesise = true) {
  439. if ($alias === null) {
  440. $alias = 'Sub' . $this->alias . '';
  441. }
  442. $fields = array($alias . '.id');
  443. $limit = null;
  444. switch ($type) {
  445. case 'count':
  446. $fields = array('COUNT(*)');
  447. break;
  448. case 'first':
  449. $limit = 1;
  450. break;
  451. }
  452. $dbo = $this->getDataSource();
  453. $default = array(
  454. 'fields' => $fields,
  455. 'table' => $dbo->fullTableName($this),
  456. 'alias' => $alias,
  457. 'limit' => $limit,
  458. 'offset' => null,
  459. 'joins' => array(),
  460. 'conditions' => array(),
  461. 'order' => null,
  462. 'group' => null
  463. );
  464. $params = array_merge($default, $options);
  465. $subQuery = trim($dbo->buildStatement($params, $this));
  466. if ($parenthesise) {
  467. $subQuery = '(' . $subQuery . ')';
  468. }
  469. return $subQuery;
  470. }
  471. /**
  472. * Wrapper find() to cache sql queries.
  473. *
  474. * @param array $conditions
  475. * @param array $fields
  476. * @param string $order
  477. * @param string $recursive
  478. * @return array
  479. * 2010-12-02 ms
  480. */
  481. public function find($type = null, $query = array()) {
  482. # reset/delete
  483. if (!empty($query['reset'])) {
  484. if (!empty($query['cache'])) {
  485. if (is_array($query['cache'])) {
  486. $key = $query['cache'][0];
  487. } else {
  488. $key = $query['cache'];
  489. if ($key === true) {
  490. $backtrace = debug_backtrace();
  491. $key = $backtrace[1]['function'];
  492. }
  493. }
  494. $this->deleteCache($key);
  495. }
  496. }
  497. # custom fixes
  498. if (is_string($type)) {
  499. switch ($type) {
  500. case 'count':
  501. if (isset($query['fields'])) {
  502. unset($query['fields']);
  503. }
  504. break;
  505. default:
  506. }
  507. }
  508. # having and group clauses enhancement
  509. if (is_array($query) && !empty($query['having']) && !empty($query['group'])) {
  510. if (!is_array($query['group'])) {
  511. $query['group'] = array($query['group']);
  512. }
  513. $ds = $this->getDataSource();
  514. $having = $ds->conditions($query['having'], true, false);
  515. $query['group'][count($query['group']) - 1] .= " HAVING $having";
  516. } /* elseif (is_array($query) && !empty($query['having'])) {
  517. $ds = $this->getDataSource();
  518. $having = $ds->conditions($query['having'], true, false);
  519. $query['conditions'][] = '1=1 HAVING '.$having;
  520. }
  521. */
  522. # find
  523. if (!Configure::read('Cache.disable') && Configure::read('Cache.check') && !empty($query['cache'])) {
  524. if (is_array($query['cache'])) {
  525. $key = $query['cache'][0];
  526. $expires = DAY;
  527. if (!empty($query['cache'][1])) {
  528. $expires = $query['cache'][1];
  529. }
  530. } else {
  531. $key = $query['cache'];
  532. if ($key === true) {
  533. $backtrace = debug_backtrace();
  534. $key = $backtrace[1]['function'];
  535. }
  536. $expires = DAY;
  537. }
  538. $options = array('prefix' => strtolower(Inflector::underscore($this->alias)) . '__', );
  539. if (!empty($expires)) {
  540. $options['duration'] = $expires;
  541. }
  542. if (!Configure::read('Cache.disable')) {
  543. Cache::config('sql', $options);
  544. $key = Inflector::underscore($key);
  545. $results = Cache::read($key, 'sql');
  546. }
  547. if ($results === null) {
  548. $results = parent::find($type, $query);
  549. Cache::write($key, $results, 'sql');
  550. }
  551. return $results;
  552. }
  553. # Without caching
  554. return parent::find($type, $query);
  555. }
  556. /*
  557. public function _findCount($state, $query, $results = array()) {
  558. if (isset($query['fields'])) {
  559. unset($query['fields']);
  560. }
  561. pr($results);
  562. return parent::_findCount($state, $query, $results = array());
  563. }
  564. */
  565. /**
  566. * This code will add formatted list functionallity to find you can easy replace the $this->Model->find('list'); with $this->Model->find('formattedlist', array('fields' => array('Model.id', 'Model.field1', 'Model.field2', 'Model.field3'), 'format' => '%s-%s %s')); and get option tag output of: Model.field1-Model.field2 Model.field3. Even better part is being able to setup your own format for the output!
  567. * @see http://bakery.cakephp.org/articles/view/add-formatted-lists-to-your-appmodel
  568. * @deprecated
  569. * added Caching
  570. * 2009-12-27 ms
  571. */
  572. protected function _find($type, $options = array()) {
  573. $res = false;
  574. if ($res === false) {
  575. if (isset($options['cache'])) {
  576. unset($options['cache']);
  577. }
  578. if (!isset($options['recursive'])) {
  579. //$options['recursive'] = -1;
  580. }
  581. switch ($type) {
  582. # @see http://bakery.cakephp.org/deu/articles/nate/2010/10/10/quick-tipp_-_doing_ad-hoc-joins_bei_model_find
  583. case 'matches':
  584. if (!isset($options['joins'])) {
  585. $options['joins'] = array();
  586. }
  587. if (!isset($options['model']) || !isset($options['scope'])) {
  588. break;
  589. }
  590. $assoc = $this->hasAndBelongsToMany[$options['model']];
  591. $bind = "{$assoc['with']}.{$assoc['foreignKey']} = {$this->alias}.{$this->primaryKey}";
  592. $options['joins'][] = array(
  593. 'table' => $assoc['joinTable'],
  594. 'alias' => $assoc['with'],
  595. 'type' => 'inner',
  596. 'foreignKey' => false,
  597. 'conditions'=> array($bind)
  598. );
  599. $bind = $options['model'] . '.' . $this->{$options['model']}->primaryKey . ' = ';
  600. $bind .= "{$assoc['with']}.{$assoc['associationForeignKey']}";
  601. $options['joins'][] = array(
  602. 'table' => $this->{$options['model']}->table,
  603. 'alias' => $options['model'],
  604. 'type' => 'inner',
  605. 'foreignKey' => false,
  606. 'conditions'=> array($bind) + (array)$options['scope'],
  607. );
  608. unset($options['model'], $options['scope']);
  609. $type = 'all';
  610. break;
  611. # probably deprecated since "virtual fields" in 1.3
  612. case 'formattedlist':
  613. if (!isset($options['fields']) || count($options['fields']) < 3) {
  614. $res = parent::find('list', $options);
  615. break;
  616. }
  617. $this->recursive = -1;
  618. //setup formating
  619. $format = '';
  620. if (!isset($options['format'])) {
  621. for ($i = 0; $i < (count($options['fields']) - 1); $i++) $format .= '%s ';
  622. $format = substr($format, 0, -1);
  623. } else {
  624. $format = $options['format'];
  625. }
  626. //get data
  627. $list = parent::find('all', $options);
  628. // remove model alias from strings to only get field names
  629. $tmpPath2[] = $format;
  630. for ($i = 1; $i <= (count($options['fields']) - 1); $i++) {
  631. $field[$i] = str_replace($this->alias . '.', '', $options['fields'][$i]);
  632. $tmpPath2[] = '{n}.' . $this->alias . '.' . $field[$i];
  633. }
  634. //do the magic?? read the code...
  635. $res = Set::combine($list, '{n}.' . $this->alias . '.' . $this->primaryKey, $tmpPath2);
  636. break;
  637. default:
  638. $res = parent::find($type, $options);
  639. break;
  640. }
  641. if (!empty($this->useCache)) {
  642. Cache::write($this->cacheName, $res, $this->cacheConfig);
  643. if (Configure::read('debug') > 0) {
  644. $this->log('WRITE (' . $this->cacheConfig . '): ' . $this->cacheName, 'cache');
  645. }
  646. }
  647. } else {
  648. if (Configure::read('debug') > 0) {
  649. $this->log('READ (' . $this->cacheConfig . '): ' . $this->cacheName, 'cache');
  650. }
  651. }
  652. return $res;
  653. }
  654. /*
  655. USAGE of formattetlist:
  656. $this->Model->find('formattedlist',
  657. array(
  658. 'fields'=>array(
  659. 'Model.id', // allows start with the value="" tags field
  660. 'Model.field1', // then put them in order of how you want the format to output.
  661. 'Model.field2',
  662. 'Model.field3',
  663. 'Model.field4',
  664. 'Model.field5',
  665. ),
  666. 'format'=>'%s-%s%s %s%s'
  667. )
  668. );
  669. */
  670. /*
  671. neighbor find problem:
  672. This means it will sort the results on Model.created ASC and DESC.
  673. However, in certain situations you would like to order on more than one
  674. field. For example, on a rating and a uploaddate. Requirements could look
  675. like: Get next en previous record of a certain Model based on the top
  676. rated. When the rating is equal those should be ordered on creation date.
  677. I suggest something similar to:
  678. $this->Movie->find('neighbors', array(
  679. 'scope' => array(
  680. array(
  681. 'field' => 'rating',
  682. 'order' => 'DESC',
  683. 'value' => 4.85
  684. ),
  685. array(
  686. 'field' => 'created',
  687. 'order' => 'DESC',
  688. 'value' => '2009-05-26 06:20:03'
  689. )
  690. )
  691. 'conditions' => array(
  692. 'approved' => true,
  693. 'processed' => true
  694. )
  695. */
  696. /**
  697. * Core-fix for multiple sort orders
  698. *
  699. * @param addiotional 'scope'=>array(field,order) - value is retrieved by (submitted) primary key
  700. * @return mixed
  701. * TODO: fix it
  702. * 2009-07-25 ms
  703. */
  704. protected function _findNeighbors($state, $query, $results = array()) {
  705. return parent::_findNeighbors($state, $query, $results);
  706. if (isset($query['scope'])) {
  707. //TODO
  708. }
  709. return parent::find($type, $options);
  710. }
  711. /**
  712. * @param mixed $id: id only, or request array
  713. * @param array $options
  714. * - filter: open/closed/none
  715. * - field (sortField, if not id)
  716. * - reverse: sortDirection (0=normalAsc/1=reverseDesc)
  717. * - displayField: ($this->displayField, if empty)
  718. * @param array $qryOptions
  719. * - recursive (defaults to -1)
  720. * TODO: try to use core function, TRY TO ALLOW MULTIPLE SORT FIELDS
  721. * @return array
  722. */
  723. public function neighbors($id = null, $options = array(), $qryOptions = array()) {
  724. $sortField = (!empty($options['field']) ? $options['field'] : 'created');
  725. $normalDirection = (!empty($options['reverse']) ? false : true);
  726. $sortDirWord = $normalDirection ? array('ASC', 'DESC') : array('DESC', 'ASC');
  727. $sortDirSymb = $normalDirection ? array('>=', '<=') : array('<=', '>=');
  728. $displayField = (!empty($options['displayField']) ? $options['displayField'] : $this->displayField);
  729. if (is_array($id)) {
  730. $data = $id;
  731. $id = $data[$this->alias]['id'];
  732. } elseif ($id === null) {
  733. $id = $this->id;
  734. }
  735. if (!empty($id)) {
  736. $data = $this->find('first', array('conditions' => array('id' => $id), 'contain' => array()));
  737. }
  738. if (empty($id) || empty($data) || empty($data[$this->alias][$sortField])) {
  739. return array();
  740. } else {
  741. $field = $data[$this->alias][$sortField];
  742. }
  743. $findOptions = array('recursive' => -1);
  744. if (isset($qryOptions['recursive'])) {
  745. $findOptions['recursive'] = $qryOptions['recursive'];
  746. }
  747. if (isset($qryOptions['contain'])) {
  748. $findOptions['contain'] = $qryOptions['contain'];
  749. }
  750. $findOptions['fields'] = array($this->alias . '.id', $this->alias . '.' . $displayField);
  751. $findOptions['conditions'][$this->alias . '.id !='] = $id;
  752. # //TODO: take out
  753. if (!empty($options['filter']) && $options['filter'] == REQUEST_STATUS_FILTER_OPEN) {
  754. $findOptions['conditions'][$this->alias . '.status <'] = REQUEST_STATUS_DECLINED;
  755. } elseif (!empty($options['filter']) && $options['filter'] == REQUEST_STATUS_FILTER_CLOSED) {
  756. $findOptions['conditions'][$this->alias . '.status >='] = REQUEST_STATUS_DECLINED;
  757. }
  758. $return = array();
  759. if (!empty($qryOptions['conditions'])) {
  760. $findOptions['conditions'] = Set::merge($findOptions['conditions'], $qryOptions['conditions']);
  761. }
  762. $options = $findOptions;
  763. $options['conditions'] = Set::merge($options['conditions'], array($this->alias . '.' . $sortField . ' ' . $sortDirSymb[1] => $field));
  764. $options['order'] = array($this->alias . '.' . $sortField . '' => $sortDirWord[1]);
  765. $this->id = $id;
  766. $return['prev'] = $this->find('first', $options);
  767. $options = $findOptions;
  768. $options['conditions'] = Set::merge($options['conditions'], array($this->alias . '.' . $sortField . ' ' . $sortDirSymb[0] => $field));
  769. $options['order'] = array($this->alias . '.' . $sortField . '' => $sortDirWord[0]); // ??? why 0 instead of 1
  770. $this->id = $id;
  771. $return['next'] = $this->find('first', $options);
  772. return $return;
  773. }
  774. /**
  775. * Delete all records using an atomic query similar to updateAll().
  776. * Note: Does not need manual sanitizing/escaping, though.
  777. *
  778. * Does not do any callbacks
  779. *
  780. * @param mixed $conditions Conditions to match, true for all records
  781. * @return boolean Success
  782. */
  783. public function deleteAllRaw($conditions = true) {
  784. return $this->getDataSource()->delete($this, $conditions);
  785. }
  786. /** Validation Functions **/
  787. /**
  788. * Overwrite invalidate to allow last => true
  789. *
  790. * @param string $field The name of the field to invalidate
  791. * @param mixed $value Name of validation rule that was not failed, or validation message to
  792. * be returned. If no validation key is provided, defaults to true.
  793. * @param boolean $last If this should be the last validation check for this validation run
  794. * @return void
  795. */
  796. public function invalidate($field, $value = true, $last = false) {
  797. parent::invalidate($field, $value);
  798. if (!$last) {
  799. return;
  800. }
  801. $this->validator()->remove($field);
  802. }
  803. /**
  804. * Validates a primary or foreign key depending on the current schema data for this field
  805. * recognizes uuid (char36) and aiid (int10 unsigned) - not yet mixed (varchar36)
  806. * more useful than using numeric or notEmpty which are type specific
  807. *
  808. * @param array $data
  809. * @param array $options
  810. * - allowEmpty
  811. * @return boolean Success
  812. * 2011-06-21 ms
  813. */
  814. public function validateKey($data = array(), $options = array()) {
  815. $keys = array_keys($data);
  816. $key = array_shift($keys);
  817. $value = array_shift($data);
  818. $schema = $this->schema($key);
  819. if (!$schema) {
  820. return true;
  821. }
  822. $defaults = array(
  823. 'allowEmpty' => false,
  824. );
  825. $options = array_merge($defaults, $options);
  826. if ($schema['type'] !== 'integer') {
  827. if ($options['allowEmpty'] && $value === '') {
  828. return true;
  829. }
  830. return Validation::uuid($value);
  831. }
  832. if ($options['allowEmpty'] && $value === 0) {
  833. return true;
  834. }
  835. return is_numeric($value) && (int)$value == $value && $value > 0;
  836. }
  837. /**
  838. * Checks if the passed enum value is valid
  839. *
  840. * @return boolean Success
  841. * 2010-02-09 ms
  842. */
  843. public function validateEnum(array $data, $enum = null, $additionalKeys = array()) {
  844. $keys = array_keys($data);
  845. $valueKey = array_shift($keys);
  846. $value = $data[$valueKey];
  847. $keys = array();
  848. if ($enum === true) {
  849. $enum = $valueKey;
  850. }
  851. if ($enum !== null) {
  852. if (!method_exists($this, $enum)) {
  853. trigger_error('Enum method \'' . $enum . '()\' not exists', E_USER_ERROR);
  854. return false;
  855. }
  856. //TODO: make static
  857. $keys = $this->{$enum}();
  858. }
  859. $keys = array_merge($additionalKeys, array_keys($keys));
  860. if (!empty($keys) && in_array($value, $keys)) {
  861. return true;
  862. }
  863. return false;
  864. }
  865. /**
  866. * Checks if the content of 2 fields are equal
  867. * Does not check on empty fields! Return TRUE even if both are empty (secure against empty in another rule)!
  868. *
  869. * @return boolean Success
  870. * 2009-01-22 ms
  871. */
  872. public function validateIdentical($data = array(), $compareWith = null, $options = array()) {
  873. if (is_array($data)) {
  874. $value = array_shift($data);
  875. } else {
  876. $value = $data;
  877. }
  878. $compareValue = $this->data[$this->alias][$compareWith];
  879. $matching = array('string' => 'string', 'int' => 'integer', 'float' => 'float', 'bool' => 'boolean');
  880. if (!empty($options['cast']) && array_key_exists($options['cast'], $matching)) {
  881. # cast values to string/int/float/bool if desired
  882. settype($compareValue, $matching[$options['cast']]);
  883. settype($value, $matching[$options['cast']]);
  884. }
  885. return ($compareValue === $value);
  886. }
  887. /**
  888. * Checks a record, if it is unique - depending on other fields in this table (transfered as array)
  889. * example in model: 'rule' => array ('validateUnique', array('belongs_to_table_id','some_id','user_id')),
  890. * if all keys (of the array transferred) match a record, return false, otherwise true
  891. *
  892. * @param array $fields Other fields to depend on
  893. * TODO: add possibity of deep nested validation (User -> Comment -> CommentCategory: UNIQUE comment_id, Comment.user_id)
  894. * @param array $options
  895. * - requireDependentFields Require all dependent fields for the validation rule to return true
  896. * @return boolean Success
  897. * 2010-01-30 ms
  898. */
  899. public function validateUnique($data, $fields = array(), $options = array()) {
  900. $id = (!empty($this->data[$this->alias]['id']) ? $this->data[$this->alias]['id'] : 0);
  901. if (!$id && $this->id) {
  902. $id = $this->id;
  903. }
  904. foreach ($data as $key => $value) {
  905. $fieldName = $key;
  906. $fieldValue = $value;
  907. break;
  908. }
  909. $conditions = array(
  910. $this->alias . '.' . $fieldName => $fieldValue,
  911. $this->alias . '.id !=' => $id);
  912. # careful, if fields is not manually filled, the options will be the second param!!! big problem...
  913. $fields = (array)$fields;
  914. if (!array_key_exists('allowEmpty', $fields)) {
  915. foreach ($fields as $dependingField) {
  916. if (isset($this->data[$this->alias][$dependingField])) { // add ONLY if some content is transfered (check on that first!)
  917. $conditions[$this->alias . '.' . $dependingField] = $this->data[$this->alias][$dependingField];
  918. } elseif (isset($this->data['Validation'][$dependingField])) { // add ONLY if some content is transfered (check on that first!
  919. $conditions[$this->alias . '.' . $dependingField] = $this->data['Validation'][$dependingField];
  920. } elseif (!empty($id)) {
  921. # manual query! (only possible on edit)
  922. $res = $this->find('first', array('fields' => array($this->alias . '.' . $dependingField), 'conditions' => array($this->alias . '.id' => $id)));
  923. if (!empty($res)) {
  924. $conditions[$this->alias . '.' . $dependingField] = $res[$this->alias][$dependingField];
  925. }
  926. } else {
  927. if (!empty($options['requireDependentFields'])) {
  928. trigger_error('Required field ' . $dependingField . ' for validateUnique validation not present');
  929. return false;
  930. }
  931. return true;
  932. }
  933. }
  934. }
  935. $this->recursive = -1;
  936. if (count($conditions) > 2) {
  937. $this->recursive = 0;
  938. }
  939. $res = $this->find('first', array('fields' => array($this->alias . '.id'), 'conditions' => $conditions));
  940. return empty($res);
  941. }
  942. /**
  943. * Alterative for validating unique fields.
  944. *
  945. * @param array $data
  946. * @param array $options
  947. * - scope (array of other fields as scope - isUnique dependent on other fields of the table)
  948. * - batch (defaults to true, remembers previous values in order to validate batch imports)
  949. * example in model: 'rule' => array ('validateUniqueExt', array('scope'=>array('belongs_to_table_id','some_id','user_id'))),
  950. * http://groups.google.com/group/cake-php/browse_thread/thread/880ee963456739ec
  951. * //TODO: test!!!
  952. * @return boolean Success
  953. * @deprecated in favor of validateUnique?
  954. * 2011-03-27 ms
  955. */
  956. public function validateUniqueExt($data, $options = array()) {
  957. foreach ($data as $key => $value) {
  958. $fieldName = $key;
  959. $fieldValue = $value;
  960. }
  961. $defaults = array('batch' => true, 'scope' => array());
  962. $options = array_merge($defaults, $options);
  963. # for batch
  964. if ($options['batch'] !== false && !empty($this->batchRecords)) {
  965. if (array_key_exists($value, $this->batchRecords[$fieldName])) {
  966. return $options['scope'] === $this->batchRecords[$fieldName][$value];
  967. }
  968. }
  969. # continue with validation
  970. if (!$this->validateUnique($data, $options['scope'])) {
  971. return false;
  972. }
  973. # for batch
  974. if ($options['batch'] !== false) {
  975. if (!isset($this->batchRecords)) {
  976. $this->batchRecords = array();
  977. }
  978. $this->batchRecords[$fieldName][$value] = $scope;
  979. }
  980. return true;
  981. }
  982. /**
  983. * Checks if a url is valid AND accessable (returns false otherwise)
  984. *
  985. * @param array/string $data: full url(!) starting with http://...
  986. * @options array
  987. * - allowEmpty TRUE/FALSE (TRUE: if empty => return TRUE)
  988. * - required TRUE/FALSE (TRUE: overrides allowEmpty)
  989. * - autoComplete (default: TRUE)
  990. * - deep (default: TRUE)
  991. * @return boolean Success
  992. * 2010-10-18 ms
  993. */
  994. public function validateUrl($data, $options = array()) {
  995. if (is_array($data)) {
  996. $url = array_shift($data);
  997. } else {
  998. $url = $data;
  999. }
  1000. if (empty($url)) {
  1001. if (!empty($options['allowEmpty']) && empty($options['required'])) {
  1002. return true;
  1003. }
  1004. return false;
  1005. }
  1006. if (!isset($options['autoComplete']) || $options['autoComplete'] !== false) {
  1007. $url = $this->_autoCompleteUrl($url);
  1008. }
  1009. if (!isset($options['strict']) || $options['strict'] !== false) {
  1010. $options['strict'] = true;
  1011. }
  1012. # validation
  1013. if (!Validation::url($url, $options['strict']) && env('REMOTE_ADDR') !== '127.0.0.1') {
  1014. return false;
  1015. }
  1016. # same domain?
  1017. if (!empty($options['sameDomain']) && !empty($_SERVER['HTTP_HOST'])) {
  1018. $is = parse_url($url, PHP_URL_HOST);
  1019. $expected = env('HTTP_HOST');
  1020. if (mb_strtolower($is) !== mb_strtolower($expected)) {
  1021. return false;
  1022. }
  1023. }
  1024. if (isset($options['deep']) && $options['deep'] === false) {
  1025. return true;
  1026. }
  1027. return $this->_validUrl($url);
  1028. }
  1029. /**
  1030. * prepend protocol if missing
  1031. *
  1032. * @param string $url
  1033. * @return string Url
  1034. * 2009-02-27 ms
  1035. */
  1036. protected function _autoCompleteUrl($url) {
  1037. if (mb_strpos($url, '/') === 0) {
  1038. $url = Router::url($url, true);
  1039. } elseif (mb_strpos($url, '://') === false && mb_strpos($url, 'www.') === 0) {
  1040. $url = 'http://' . $url;
  1041. }
  1042. return $url;
  1043. }
  1044. /**
  1045. * checks if a url is valid
  1046. *
  1047. * @param string url
  1048. * @return boolean Success
  1049. * 2009-02-27 ms
  1050. */
  1051. protected function _validUrl($url) {
  1052. $headers = Utility::getHeaderFromUrl($url);
  1053. if ($headers === false) {
  1054. return false;
  1055. }
  1056. $headers = implode("\n", $headers);
  1057. $protocol = mb_strpos($url, 'https://') === 0 ? 'HTTP' : 'HTTP';
  1058. if (!preg_match('#^' . $protocol . '/.*?\s+[(200|301|302)]+\s#i', $headers)) {
  1059. return false;
  1060. }
  1061. if (preg_match('#^' . $protocol . '/.*?\s+[(404|999)]+\s#i', $headers)) {
  1062. return false;
  1063. }
  1064. return true;
  1065. }
  1066. /**
  1067. * Validation of DateTime Fields (both Date and Time together)
  1068. *
  1069. * @param options
  1070. * - dateFormat (defaults to 'ymd')
  1071. * - allowEmpty
  1072. * - after/before (fieldName to validate against)
  1073. * - min/max (defaults to >= 1 - at least 1 minute apart)
  1074. * @return boolean Success
  1075. * 2011-03-02 ms
  1076. */
  1077. public function validateDateTime($data, $options = array()) {
  1078. $format = !empty($options['dateFormat']) ? $options['dateFormat'] : 'ymd';
  1079. if (is_array($data)) {
  1080. $value = array_shift($data);
  1081. } else {
  1082. $value = $data;
  1083. }
  1084. $dateTime = explode(' ', trim($value), 2);
  1085. $date = $dateTime[0];
  1086. $time = (!empty($dateTime[1]) ? $dateTime[1] : '');
  1087. if (!empty($options['allowEmpty']) && (empty($date) && empty($time) || $date == DEFAULT_DATE && $time == DEFAULT_TIME || $date == DEFAULT_DATE && empty($time))) {
  1088. return true;
  1089. }
  1090. /*
  1091. if ($this->validateDate($date, $options) && $this->validateTime($time, $options)) {
  1092. return true;
  1093. }
  1094. */
  1095. if (Validation::date($date, $format) && Validation::time($time)) {
  1096. # after/before?
  1097. $minutes = isset($options['min']) ? $options['min'] : 1;
  1098. if (!empty($options['after']) && isset($this->data[$this->alias][$options['after']])) {
  1099. if (strtotime($this->data[$this->alias][$options['after']]) > strtotime($value) - $minutes) {
  1100. return false;
  1101. }
  1102. }
  1103. if (!empty($options['before']) && isset($this->data[$this->alias][$options['before']])) {
  1104. if (strtotime($this->data[$this->alias][$options['before']]) < strtotime($value) + $minutes) {
  1105. return false;
  1106. }
  1107. }
  1108. return true;
  1109. }
  1110. return false;
  1111. }
  1112. /**
  1113. * Validation of Date fields (as the core one is buggy!!!)
  1114. *
  1115. * @param options
  1116. * - dateFormat (defaults to 'ymd')
  1117. * - allowEmpty
  1118. * - after/before (fieldName to validate against)
  1119. * - min (defaults to 0 - equal is OK too)
  1120. * @return boolean Success
  1121. * 2011-03-02 ms
  1122. */
  1123. public function validateDate($data, $options = array()) {
  1124. $format = !empty($options['format']) ? $options['format'] : 'ymd';
  1125. if (is_array($data)) {
  1126. $value = array_shift($data);
  1127. } else {
  1128. $value = $data;
  1129. }
  1130. $dateTime = explode(' ', trim($value), 2);
  1131. $date = $dateTime[0];
  1132. if (!empty($options['allowEmpty']) && (empty($date) || $date == DEFAULT_DATE)) {
  1133. return true;
  1134. }
  1135. if (Validation::date($date, $format)) {
  1136. # after/before?
  1137. $days = !empty($options['min']) ? $options['min'] : 0;
  1138. if (!empty($options['after']) && isset($this->data[$this->alias][$options['after']])) {
  1139. if ($this->data[$this->alias][$options['after']] > date(FORMAT_DB_DATE, strtotime($date) - $days * DAY)) {
  1140. return false;
  1141. }
  1142. }
  1143. if (!empty($options['before']) && isset($this->data[$this->alias][$options['before']])) {
  1144. if ($this->data[$this->alias][$options['before']] < date(FORMAT_DB_DATE, strtotime($date) + $days * DAY)) {
  1145. return false;
  1146. }
  1147. }
  1148. return true;
  1149. }
  1150. return false;
  1151. }
  1152. /**
  1153. * Validation of Time fields
  1154. *
  1155. * @param array $options
  1156. * - timeFormat (defaults to 'hms')
  1157. * - allowEmpty
  1158. * - after/before (fieldName to validate against)
  1159. * - min/max (defaults to >= 1 - at least 1 minute apart)
  1160. * @return boolean Success
  1161. * 2011-03-02 ms
  1162. */
  1163. public function validateTime($data, $options = array()) {
  1164. if (is_array($data)) {
  1165. $value = array_shift($data);
  1166. } else {
  1167. $value = $data;
  1168. }
  1169. $dateTime = explode(' ', trim($value), 2);
  1170. $value = array_pop($dateTime);
  1171. if (Validation::time($value)) {
  1172. # after/before?
  1173. if (!empty($options['after']) && isset($this->data[$this->alias][$options['after']])) {
  1174. if ($this->data[$this->alias][$options['after']] >= $value) {
  1175. return false;
  1176. }
  1177. }
  1178. if (!empty($options['before']) && isset($this->data[$this->alias][$options['before']])) {
  1179. if ($this->data[$this->alias][$options['before']] <= $value) {
  1180. return false;
  1181. }
  1182. }
  1183. return true;
  1184. }
  1185. return false;
  1186. }
  1187. /**
  1188. * Validation of Date Fields (>= minDate && <= maxDate)
  1189. * @param options
  1190. * - min/max (TODO!!)
  1191. * 2010-01-20 ms
  1192. */
  1193. public function validateDateRange($data, $options = array()) {
  1194. }
  1195. //TODO
  1196. /**
  1197. * Validation of Time Fields (>= minTime && <= maxTime)
  1198. * @param options
  1199. * - min/max (TODO!!)
  1200. * 2010-01-20 ms
  1201. */
  1202. public function validateTimeRange($data, $options = array()) {
  1203. }
  1204. /**
  1205. * model validation rule for email addresses
  1206. *
  1207. * @return boolean Success
  1208. * 2010-01-14 ms
  1209. */
  1210. public function validateUndisposable($data, $proceed = false) {
  1211. $email = array_shift($data);
  1212. if (empty($email)) {
  1213. return true;
  1214. }
  1215. return $this->isUndisposableEmail($email, false, $proceed);
  1216. }
  1217. /**
  1218. * NOW: can be set to work offline only (if server is down etc)
  1219. * Checks if a email is not from a garbige hoster
  1220. *
  1221. * @param string email (necessary)
  1222. * @return boolean true if valid, else false
  1223. * 2009-03-09 ms
  1224. */
  1225. public function isUndisposableEmail($email, $onlineMode = false, $proceed = false) {
  1226. if (!isset($this->UndisposableEmail)) {
  1227. App::import('Vendor', 'undisposable/undisposable');
  1228. $this->UndisposableEmail = new UndisposableEmail();
  1229. }
  1230. if (!$onlineMode) {
  1231. # crashed with white screen of death otherwise... (if foreign page is 404)
  1232. $this->UndisposableEmail->useOnlineList(false);
  1233. }
  1234. if (!class_exists('Validation')) {
  1235. App::uses('Validation', 'Utility');
  1236. }
  1237. if (!Validation::email($email)) {
  1238. return false;
  1239. }
  1240. if ($this->UndisposableEmail->isUndisposableEmail($email) === false) {
  1241. # trigger log
  1242. $this->log('Disposable Email detected: ' . h($email).' (IP '.env('REMOTE_ADDR').')', 'undisposable');
  1243. if ($proceed === true) {
  1244. return true;
  1245. }
  1246. return false;
  1247. }
  1248. return true;
  1249. }
  1250. /**
  1251. * Is blocked email?
  1252. * //TODO: move outside of MyModel?
  1253. *
  1254. * @return boolean ifNotBlacklisted
  1255. * 2009-12-22 ms
  1256. */
  1257. public function validateNotBlocked($params) {
  1258. $email = array_shift($params);
  1259. if (!isset($this->Blacklist)) {
  1260. //App::uses('Blacklist', 'Tools.Model'
  1261. $this->Blacklist = ClassRegistry::init('Tools.Blacklist');
  1262. }
  1263. if ($this->Blacklist->isBlacklisted(Blacklist::TYPE_EMAIL, $email)) {
  1264. return false;
  1265. }
  1266. return true;
  1267. }
  1268. /** General Model Functions **/
  1269. /**
  1270. * CAREFUL: use LIMIT due to Starker Serverlastigkeit! or CACHE it!
  1271. *
  1272. * e.g.: 'ORDER BY ".$this->umlautsOrderFix('User.nic')." ASC'
  1273. *
  1274. * @param string variable (to be correctly ordered)
  1275. * @deprecated
  1276. */
  1277. public function umlautsOrderFix($var) {
  1278. return "REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(".$var.", 'Ä', 'Ae'), 'Ö', 'Oe'), 'Ü', 'Ue'), 'ä', 'ae'), 'ö', 'oe'), 'ü','ue'), 'ß', 'ss')";
  1279. }
  1280. /**
  1281. * set + guaranteeFields!
  1282. * Extends the core set function (only using data!!!)
  1283. *
  1284. * @param mixed $data
  1285. * @param mixed $data2 (optional)
  1286. * @param array $requiredFields Required fields
  1287. * @param array $fieldList Whitelist / Allowed fields
  1288. * @return array
  1289. * 2010-03-11 ms
  1290. */
  1291. public function set($data, $data2 = null, $requiredFields = array(), $fieldList = array()) {
  1292. if (!empty($requiredFields)) {
  1293. $data = $this->guaranteeFields($requiredFields, $data);
  1294. }
  1295. if (!empty($fieldList)) {
  1296. $data = $this->whitelist($fieldList, $data);
  1297. }
  1298. return parent::set($data, $data2);
  1299. }
  1300. /**
  1301. * @param array $fieldList
  1302. * @param array $data (optional)
  1303. * @return array
  1304. * 2011-06-01 ms
  1305. */
  1306. public function whitelist($fieldList, $data = null) {
  1307. $model = $this->alias;
  1308. if ($data === null) {
  1309. $data = $this->data;
  1310. }
  1311. foreach ($data[$model] as $key => $val) {
  1312. if (!in_array($key, $fieldList)) {
  1313. unset($data[$model][$key]);
  1314. }
  1315. }
  1316. return $data;
  1317. }
  1318. /**
  1319. * make sure required fields exists - in order to properly validate them
  1320. * @param array: field1, field2 - or field1, Model2.field1 etc
  1321. * @param array: data (optional, otherwise the array with the required fields will be returned)
  1322. * @return array
  1323. * 2010-03-11 ms
  1324. */
  1325. public function guaranteeFields($requiredFields, $data = null) {
  1326. $guaranteedFields = array();
  1327. foreach ($requiredFields as $column) {
  1328. if (strpos($column, '.') !== false) {
  1329. list($model, $column) = explode('.', $column, 2);
  1330. } else {
  1331. $model = $this->alias;
  1332. }
  1333. $guaranteedFields[$model][$column] = ''; # now field exists in any case!
  1334. }
  1335. if ($data === null) {
  1336. return $guaranteedFields;
  1337. }
  1338. if (!empty($guaranteedFields)) {
  1339. $data = Set::merge($guaranteedFields, $data);
  1340. }
  1341. return $data;
  1342. }
  1343. /**
  1344. * make certain fields a requirement for the form to validate
  1345. * (they must only be present - can still be empty, though!)
  1346. *
  1347. * @param array $fieldList
  1348. * @param boolean $allowEmpty (or NULL to not touch already set elements)
  1349. * @return void
  1350. * 2012-02-20 ms
  1351. */
  1352. public function requireFields($requiredFields, $allowEmpty = null) {
  1353. if ($allowEmpty === null) {
  1354. $setAllowEmpty = true;
  1355. } else {
  1356. $setAllowEmpty = $allowEmpty;
  1357. }
  1358. foreach ($requiredFields as $column) {
  1359. if (strpos($column, '.') !== false) {
  1360. list($model, $column) = explode('.', $column, 2);
  1361. } else {
  1362. $model = $this->alias;
  1363. }
  1364. if ($model !== $this->alias) {
  1365. continue;
  1366. }
  1367. if (empty($this->validate[$column])) {
  1368. $this->validate[$column]['notEmpty'] = array('rule' => 'notEmpty', 'required' => true, 'allowEmpty' => $setAllowEmpty, 'message' => 'valErrMandatoryField');
  1369. } else {
  1370. $keys = array_keys($this->validate[$column]);
  1371. if (!in_array('rule', $keys)) {
  1372. $key = array_shift($keys);
  1373. $this->validate[$column][$key]['required'] = true;
  1374. if (!isset($this->validate[$column][$key]['allowEmpty'])) {
  1375. $this->validate[$column][$key]['allowEmpty'] = $setAllowEmpty;
  1376. }
  1377. } else {
  1378. $keys['required'] = true;
  1379. if (!isset($keys['allowEmpty'])) {
  1380. $keys['allowEmpty'] = $setAllowEmpty;
  1381. }
  1382. $this->validate[$column] = $keys;
  1383. }
  1384. }
  1385. }
  1386. }
  1387. /**
  1388. * instead of whitelisting
  1389. * @param array $blackList
  1390. * - array: fields to blacklist
  1391. * - boolean TRUE: removes all foreign_keys (_id and _key)
  1392. * note: one-dimensional
  1393. * @return array
  1394. * 2009-06-19 ms
  1395. */
  1396. public function blacklist($blackList = array()) {
  1397. if ($blackList === true) {
  1398. //TODO
  1399. }
  1400. return array_diff(array_keys($this->schema()), (array)$blackList);
  1401. }
  1402. /**
  1403. * Shortcut method to find a specific entry via primary key
  1404. *
  1405. * @param mixed $id
  1406. * @param string|array $fields
  1407. * @param array $contain
  1408. * @return mixed
  1409. * 2009-11-14 ms
  1410. */
  1411. public function get($id, $fields = array(), $contain = array()) {
  1412. if (is_array($id)) {
  1413. $column = $id[0];
  1414. $value = $id[1];
  1415. } else {
  1416. $column = 'id';
  1417. $value = $id;
  1418. }
  1419. if ($fields === '*') {
  1420. $fields = $this->alias . '.*';
  1421. } elseif (!empty($fields)) {
  1422. foreach ($fields as $row => $field) {
  1423. if (strpos($field, '.') !== false) {
  1424. continue;
  1425. }
  1426. $fields[$row] = $this->alias . '.' . $field;
  1427. }
  1428. }
  1429. $options = array(
  1430. 'conditions' => array($this->alias . '.' . $column => $value),
  1431. );
  1432. if (!empty($fields)) {
  1433. $options['fields'] = $fields;
  1434. }
  1435. if (!empty($contain)) {
  1436. $options['contain'] = $contain;
  1437. }
  1438. return $this->find('first', $options);
  1439. }
  1440. /**
  1441. * Get all related entries that have been used so far
  1442. *
  1443. * @param string $modelName The related model
  1444. * @param string $groupField Field to group by
  1445. * @param string $type Find type
  1446. * @param array $options
  1447. * @return array
  1448. */
  1449. public function getRelatedInUse($modelName, $groupField, $type = 'all', $options = array()) {
  1450. $defaults = array(
  1451. 'contain' => array($modelName),
  1452. 'group' => $groupField,
  1453. 'order' => $this->$modelName->order ? $this->$modelName->order : array($modelName . '.' . $this->$modelName->displayField => 'ASC'),
  1454. );
  1455. if ($type === 'list') {
  1456. $defaults['fields'] = array($modelName . '.' . $this->$modelName->primaryKey, $modelName . '.' . $this->$modelName->displayField);
  1457. }
  1458. $options += $defaults;
  1459. return $this->find($type, $options);
  1460. }
  1461. /**
  1462. * Get all fields that have been used so far
  1463. *
  1464. * @param string $groupField Field to group by
  1465. * @param string $type Find type
  1466. * @param array $options
  1467. * @return array
  1468. */
  1469. public function getFieldInUse($groupField, $type = 'all', $options = array()) {
  1470. $defaults = array(
  1471. 'group' => $groupField,
  1472. 'order' => array($this->alias . '.' . $this->displayField => 'ASC'),
  1473. );
  1474. if ($type === 'list') {
  1475. $defaults['fields'] = array($this->alias . '.' . $this->primaryKey, $this->alias . '.' . $this->displayField);
  1476. }
  1477. $options += $defaults;
  1478. return $this->find($type, $options);
  1479. }
  1480. /**
  1481. * Update a row with certain fields (dont use "Model" as super-key)
  1482. * @param integer $id
  1483. * @param array $data
  1484. * @return boolean|array Success
  1485. * 2012-11-20 ms
  1486. */
  1487. public function update($id, $data, $validate = false) {
  1488. $this->id = $id;
  1489. return $this->save($data, $validate, array_keys($data));
  1490. }
  1491. /**
  1492. * Automagic increasing of a field with e.g.:
  1493. * $this->id = ID; $this->inc('weight',3);
  1494. *
  1495. * @deprecated use atomic updateAll() instead!
  1496. * @param string fieldname
  1497. * @param integer factor: defaults to 1 (could be negative as well - if field is signed and can be < 0)
  1498. */
  1499. public function inc($field, $factor = 1) {
  1500. $value = Set::extract($this->read($field), $this->alias . '.' . $field);
  1501. $value += $factor;
  1502. return $this->saveField($field, $value);
  1503. }
  1504. /**
  1505. * Toggles Field (Important/Deleted/Primary etc)
  1506. * @param STRING fieldName
  1507. * @param integer id (cleaned!)
  1508. * @return ARRAY record: [Model][values],...
  1509. * AJAX?
  1510. * 2008-11-06 ms
  1511. */
  1512. public function toggleField($fieldName, $id) {
  1513. $record = $this->get($id, array('id', $fieldName));
  1514. if (!empty($record) && !empty($fieldName) && $this->hasField($fieldName)) {
  1515. $record[$this->alias][$fieldName] = ($record[$this->alias][$fieldName] == 1 ? 0 : 1);
  1516. $this->id = $id;
  1517. $this->saveField($fieldName, $record[$this->alias][$fieldName]);
  1518. }
  1519. return $record;
  1520. }
  1521. /**
  1522. * truncate TABLE (already validated, that table exists)
  1523. * @param string table [default:FALSE = current model table]
  1524. * @return boolean Success
  1525. */
  1526. public function truncate($table = null) {
  1527. if (empty($table)) {
  1528. $table = $this->table;
  1529. }
  1530. $db = ConnectionManager::getDataSource($this->useDbConfig);
  1531. return $db->truncate($table);
  1532. }
  1533. /** Deep Lists **/
  1534. /**
  1535. * recursive Dropdown Lists
  1536. * NEEDS tree behavior, NEEDS lft, rght, parent_id (!)
  1537. * //FIXME
  1538. * 2008-01-02 ms
  1539. */
  1540. public function recursiveSelect($conditions = array(), $attachTree = false, $spacer = '-- ') {
  1541. if ($attachTree) {
  1542. $this->Behaviors->load('Tree');
  1543. }
  1544. $data = $this->generateTreeList($conditions, null, null, $spacer);
  1545. return $data;
  1546. }
  1547. /**
  1548. * from http://othy.wordpress.com/2006/06/03/generatenestedlist/
  1549. * NEEDS parent_id
  1550. * //TODO refactor for 1.2
  1551. *
  1552. * @deprecated use generateTreeList instead
  1553. * 2009-08-12 ms
  1554. */
  1555. public function generateNestedList($conditions = null, $indent = '--') {
  1556. $cats = $this->find('threaded', array('conditions' => $conditions, 'fields' => array(
  1557. $this->name . '.id',
  1558. $this->name . '.name',
  1559. $this->name . '.parent_id')));
  1560. $glist = $this->_generateNestedList($cats, $indent);
  1561. return $glist;
  1562. }
  1563. /**
  1564. * from http://othy.wordpress.com/2006/06/03/generatenestedlist/
  1565. * @deprecated use generateTreeList instead
  1566. * 2009-08-12 ms
  1567. */
  1568. public function _generateNestedList($cats, $indent = '--', $level = 0) {
  1569. static $list = array();
  1570. $c = count($cats);
  1571. for ($i = 0; $i < $c; $i++) {
  1572. $list[$cats[$i][$this->alias]['id']] = str_repeat($indent, $level) . $cats[$i][$this->alias][$this->displayField];
  1573. if (!empty($cats[$i]['children'])) {
  1574. $this->_generateNestedList($cats[$i]['children'], $indent, $level + 1);
  1575. }
  1576. }
  1577. return $list;
  1578. }
  1579. }