Table.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597
  1. <?php
  2. namespace Tools\Model\Table;
  3. use Cake\ORM\Table as CakeTable;
  4. use Cake\Validation\Validator;
  5. use Cake\Validation\Validation;
  6. use Cake\Utility\Inflector;
  7. use Cake\Core\Configure;
  8. class Table extends CakeTable {
  9. /**
  10. * initialize()
  11. *
  12. * @param mixed $config
  13. * @return void
  14. */
  15. public function initialize(array $config) {
  16. // Shims
  17. if (isset($this->primaryKey)) {
  18. $this->primaryKey($this->primaryKey);
  19. }
  20. if (isset($this->displayField)) {
  21. $this->displayField($this->displayField);
  22. }
  23. $this->_shimRelations();
  24. $this->addBehavior('Timestamp');
  25. }
  26. /**
  27. * Shim the 2.x way of class properties for relations.
  28. *
  29. * @return void
  30. */
  31. protected function _shimRelations() {
  32. if (!empty($this->belongsTo)) {
  33. foreach ($this->belongsTo as $k => $v) {
  34. if (is_int($k)) {
  35. $k = $v;
  36. $v = array();
  37. }
  38. if (!empty($v['className'])) {
  39. $v['className'] = Inflector::pluralize($v['className']);
  40. }
  41. $v = array_filter($v);
  42. $this->belongsTo(Inflector::pluralize($k), $v);
  43. }
  44. }
  45. if (!empty($this->hasOne)) {
  46. foreach ($this->hasOne as $k => $v) {
  47. if (is_int($k)) {
  48. $k = $v;
  49. $v = array();
  50. }
  51. if (!empty($v['className'])) {
  52. $v['className'] = Inflector::pluralize($v['className']);
  53. }
  54. $v = array_filter($v);
  55. $this->hasOne(Inflector::pluralize($k), $v);
  56. }
  57. }
  58. if (!empty($this->hasMany)) {
  59. foreach ($this->hasMany as $k => $v) {
  60. if (is_int($k)) {
  61. $k = $v;
  62. $v = array();
  63. }
  64. if (!empty($v['className'])) {
  65. $v['className'] = Inflector::pluralize($v['className']);
  66. }
  67. $v = array_filter($v);
  68. $this->hasMany(Inflector::pluralize($k), $v);
  69. }
  70. }
  71. if (!empty($this->hasAndBelongsToMany)) {
  72. foreach ($this->hasAndBelongsToMany as $k => $v) {
  73. if (is_int($k)) {
  74. $k = $v;
  75. $v = array();
  76. }
  77. if (!empty($v['className'])) {
  78. $v['className'] = Inflector::pluralize($v['className']);
  79. }
  80. $v = array_filter($v);
  81. $this->belongsToMany(Inflector::pluralize($k), $v);
  82. }
  83. }
  84. }
  85. /**
  86. * Shim the 2.x way of validate class properties.
  87. *
  88. * @param Validator $validator
  89. * @return Validator
  90. */
  91. public function validationDefault(Validator $validator) {
  92. if (!empty($this->validate)) {
  93. foreach ($this->validate as $field => $rules) {
  94. if (is_int($field)) {
  95. $field = $rules;
  96. $rules = array();
  97. }
  98. foreach ((array)$rules as $key => $rule) {
  99. if (isset($rule['required'])) {
  100. $validator->requirePresence($field, $rule['required']);
  101. unset($rule['required']);
  102. }
  103. if (isset($rule['allowEmpty'])) {
  104. $validator->allowEmpty($field, $rule['allowEmpty']);
  105. unset($rule['allowEmpty']);
  106. }
  107. if (isset($rule['message'])) {
  108. $rules[$key]['message'] = __($rule['message']);
  109. }
  110. }
  111. $validator->add($field, $rules);
  112. }
  113. }
  114. return $validator;
  115. }
  116. /**
  117. * Validator method used to check the uniqueness of a value for a column.
  118. * This is meant to be used with the validation API and not to be called
  119. * directly.
  120. *
  121. * ### Example:
  122. *
  123. * {{{
  124. * $validator->add('email', [
  125. * 'unique' => ['rule' => 'validateUnique', 'provider' => 'table']
  126. * ])
  127. * }}}
  128. *
  129. * Unique validation can be scoped to the value of another column:
  130. *
  131. * {{{
  132. * $validator->add('email', [
  133. * 'unique' => [
  134. * 'rule' => ['validateUnique', ['scope' => 'site_id']],
  135. * 'provider' => 'table'
  136. * ]
  137. * ]);
  138. * }}}
  139. *
  140. * In the above example, the email uniqueness will be scoped to only rows having
  141. * the same site_id. Scoping will only be used if the scoping field is present in
  142. * the data to be validated.
  143. *
  144. * @override To allow multiple scoped values
  145. *
  146. * @param mixed $value The value of column to be checked for uniqueness
  147. * @param array $options The options array, optionally containing the 'scope' key
  148. * @param array $context The validation context as provided by the validation routine
  149. * @return bool true if the value is unique
  150. */
  151. public function validateUnique($value, array $options, array $context = []) {
  152. if (empty($context)) {
  153. $context = $options;
  154. }
  155. $conditions = [$context['field'] => $value];
  156. if (!empty($options['scope'])) {
  157. foreach ((array)$options['scope'] as $scope) {
  158. if (!isset($context['data'][$scope])) {
  159. continue;
  160. }
  161. $scopedValue = $context['data'][$scope];
  162. $conditions[$scope] = $scopedValue;
  163. }
  164. }
  165. if (!$context['newRecord']) {
  166. $keys = (array)$this->primaryKey();
  167. $not = [];
  168. foreach ($keys as $key) {
  169. if (isset($context['data'][$key])) {
  170. $not[$key] = $context['data'][$key];
  171. }
  172. }
  173. $conditions['NOT'] = $not;
  174. }
  175. return !$this->exists($conditions);
  176. }
  177. /**
  178. * Checks a record, if it is unique - depending on other fields in this table (transfered as array)
  179. * example in model: 'rule' => array ('validateUnique', array('belongs_to_table_id','some_id','user_id')),
  180. * if all keys (of the array transferred) match a record, return false, otherwise true
  181. *
  182. * @param array $fields Other fields to depend on
  183. * TODO: add possibity of deep nested validation (User -> Comment -> CommentCategory: UNIQUE comment_id, Comment.user_id)
  184. * @param array $options
  185. * - requireDependentFields Require all dependent fields for the validation rule to return true
  186. * @return bool Success
  187. */
  188. public function validateUniqueExt($fieldValue, $fields = array(), $options = array()) {
  189. $id = (!empty($this->data[$this->alias][$this->primaryKey]) ? $this->data[$this->alias][$this->primaryKey] : 0);
  190. if (!$id && $this->id) {
  191. $id = $this->id;
  192. }
  193. $conditions = array(
  194. $this->alias . '.' . $fieldName => $fieldValue,
  195. $this->alias . '.id !=' => $id);
  196. $fields = (array)$fields;
  197. if (!array_key_exists('allowEmpty', $fields)) {
  198. foreach ($fields as $dependingField) {
  199. if (isset($this->data[$this->alias][$dependingField])) { // add ONLY if some content is transfered (check on that first!)
  200. $conditions[$this->alias . '.' . $dependingField] = $this->data[$this->alias][$dependingField];
  201. } elseif (isset($this->data['Validation'][$dependingField])) { // add ONLY if some content is transfered (check on that first!
  202. $conditions[$this->alias . '.' . $dependingField] = $this->data['Validation'][$dependingField];
  203. } elseif (!empty($id)) {
  204. // manual query! (only possible on edit)
  205. $res = $this->find('first', array('fields' => array($this->alias . '.' . $dependingField), 'conditions' => array($this->alias . '.id' => $id)));
  206. if (!empty($res)) {
  207. $conditions[$this->alias . '.' . $dependingField] = $res[$this->alias][$dependingField];
  208. }
  209. } else {
  210. if (!empty($options['requireDependentFields'])) {
  211. trigger_error('Required field ' . $dependingField . ' for validateUnique validation not present');
  212. return false;
  213. }
  214. return true;
  215. }
  216. }
  217. }
  218. $this->recursive = -1;
  219. if (count($conditions) > 2) {
  220. $this->recursive = 0;
  221. }
  222. $options = array('fields' => array($this->alias . '.' . $this->primaryKey), 'conditions' => $conditions);
  223. $res = $this->find('first', $options);
  224. return empty($res);
  225. }
  226. /**
  227. * Shim to provide 2.x way of find('first') for easier upgrade.
  228. *
  229. * @param string $type
  230. * @param array $options
  231. * @return Query
  232. */
  233. public function find($type = 'all', $options = []) {
  234. if ($type === 'first') {
  235. return parent::find('all', $options)->first();
  236. }
  237. if ($type === 'count') {
  238. return parent::find('all', $options)->count();
  239. }
  240. return parent::find($type, $options);
  241. }
  242. /**
  243. * Table::field()
  244. *
  245. * @param string $name
  246. * @param array $options
  247. * @return mixed Field value or null if not available
  248. */
  249. public function field($name, array $options = array()) {
  250. $result = $this->find('all', $options)->first();
  251. if (!$result) {
  252. return null;
  253. }
  254. return $result->get($name);
  255. }
  256. /**
  257. * Overwrite to allow markNew => auto
  258. *
  259. * @param array $data The data to build an entity with.
  260. * @param array $options A list of options for the object hydration.
  261. * @return \Cake\Datasource\EntityInterface
  262. */
  263. public function newEntity(array $data = [], array $options = []) {
  264. if (Configure::read('Entity.autoMarkNew')) {
  265. $options += ['markNew' => 'auto'];
  266. }
  267. if (isset($options['markNew']) && $options['markNew'] === 'auto') {
  268. $this->_primaryKey = (array)$this->primaryKey();
  269. $this->_primaryKey = $this->_primaryKey[0];
  270. $options['markNew'] = !empty($data[$this->_primaryKey]);
  271. }
  272. return parent::newEntity($data, $options);
  273. }
  274. /**
  275. * truncate()
  276. *
  277. * @return void
  278. */
  279. public function truncate() {
  280. $sql = $this->schema()->truncateSql($this->_connection);
  281. foreach ($sql as $snippet) {
  282. $this->_connection->execute($snippet);
  283. }
  284. }
  285. /**
  286. * Get all related entries that have been used so far
  287. *
  288. * @param string $modelName The related model
  289. * @param string $groupField Field to group by
  290. * @param string $type Find type
  291. * @param array $options
  292. * @return array
  293. */
  294. public function getRelatedInUse($modelName, $groupField = null, $type = 'all', $options = array()) {
  295. if ($groupField === null) {
  296. $groupField = $this->belongsTo[$modelName]['foreignKey'];
  297. }
  298. $defaults = array(
  299. 'contain' => array($modelName),
  300. 'group' => $groupField,
  301. 'order' => $this->$modelName->order ? $this->$modelName->order : array($modelName . '.' . $this->$modelName->displayField => 'ASC'),
  302. );
  303. if ($type === 'list') {
  304. $defaults['fields'] = array($modelName . '.' . $this->$modelName->primaryKey, $modelName . '.' . $this->$modelName->displayField);
  305. }
  306. $options += $defaults;
  307. return $this->find($type, $options);
  308. }
  309. /**
  310. * Get all fields that have been used so far
  311. *
  312. * @param string $groupField Field to group by
  313. * @param string $type Find type
  314. * @param array $options
  315. * @return array
  316. */
  317. public function getFieldInUse($groupField, $type = 'all', $options = array()) {
  318. $defaults = array(
  319. 'group' => $groupField,
  320. 'order' => array($this->alias . '.' . $this->displayField => 'ASC'),
  321. );
  322. if ($type === 'list') {
  323. $defaults['fields'] = array($this->alias . '.' . $this->primaryKey, $this->alias . '.' . $this->displayField);
  324. }
  325. $options += $defaults;
  326. return $this->find($type, $options);
  327. }
  328. /**
  329. * Checks if the content of 2 fields are equal
  330. * Does not check on empty fields! Return TRUE even if both are empty (secure against empty in another rule)!
  331. *
  332. * Options:
  333. * - compare: field to compare to
  334. * - cast: if casting should be applied to both values
  335. *
  336. * @param mixed $value
  337. * @param array $options
  338. * @return bool Success
  339. */
  340. public function validateIdentical($value, $options = [], array $context = []) {
  341. if (!is_array($options)) {
  342. $options = array('compare' => $options);
  343. }
  344. if (!isset($context['data'][$options['compare']])) {
  345. return false;
  346. }
  347. $compareValue = $context['data'][$options['compare']];
  348. $matching = array('string' => 'string', 'int' => 'integer', 'float' => 'float', 'bool' => 'boolean');
  349. if (!empty($options['cast']) && array_key_exists($options['cast'], $matching)) {
  350. // cast values to string/int/float/bool if desired
  351. settype($compareValue, $matching[$options['cast']]);
  352. settype($value, $matching[$options['cast']]);
  353. }
  354. return ($compareValue === $value);
  355. }
  356. /**
  357. * Checks if a url is valid AND accessable (returns false otherwise)
  358. *
  359. * @param array/string $data: full url(!) starting with http://...
  360. * @options array
  361. * - allowEmpty TRUE/FALSE (TRUE: if empty => return TRUE)
  362. * - required TRUE/FALSE (TRUE: overrides allowEmpty)
  363. * - autoComplete (default: TRUE)
  364. * - deep (default: TRUE)
  365. * @return bool Success
  366. */
  367. public function validateUrl($url, $options = array()) {
  368. if (empty($url)) {
  369. if (!empty($options['allowEmpty']) && empty($options['required'])) {
  370. return true;
  371. }
  372. return false;
  373. }
  374. if (!isset($options['autoComplete']) || $options['autoComplete'] !== false) {
  375. $url = $this->_autoCompleteUrl($url);
  376. if (isset($key)) {
  377. $this->data[$this->alias][$key] = $url;
  378. }
  379. }
  380. if (!isset($options['strict']) || $options['strict'] !== false) {
  381. $options['strict'] = true;
  382. }
  383. // validation
  384. if (!Validation::url($url, $options['strict']) && env('REMOTE_ADDR') && env('REMOTE_ADDR') !== '127.0.0.1') {
  385. return false;
  386. }
  387. // same domain?
  388. if (!empty($options['sameDomain']) && env('HTTP_HOST')) {
  389. $is = parse_url($url, PHP_URL_HOST);
  390. $expected = env('HTTP_HOST');
  391. if (mb_strtolower($is) !== mb_strtolower($expected)) {
  392. return false;
  393. }
  394. }
  395. if (isset($options['deep']) && $options['deep'] === false) {
  396. return true;
  397. }
  398. return $this->_validUrl($url);
  399. }
  400. /**
  401. * Prepend protocol if missing
  402. *
  403. * @param string $url
  404. * @return string Url
  405. */
  406. protected function _autoCompleteUrl($url) {
  407. if (mb_strpos($url, '/') === 0) {
  408. $url = Router::url($url, true);
  409. } elseif (mb_strpos($url, '://') === false && mb_strpos($url, 'www.') === 0) {
  410. $url = 'http://' . $url;
  411. }
  412. return $url;
  413. }
  414. /**
  415. * Checks if a url is valid
  416. *
  417. * @param string url
  418. * @return bool Success
  419. */
  420. protected function _validUrl($url) {
  421. $headers = Utility::getHeaderFromUrl($url);
  422. if ($headers === false) {
  423. return false;
  424. }
  425. $headers = implode("\n", $headers);
  426. $protocol = mb_strpos($url, 'https://') === 0 ? 'HTTP' : 'HTTP';
  427. if (!preg_match('#^' . $protocol . '/.*?\s+[(200|301|302)]+\s#i', $headers)) {
  428. return false;
  429. }
  430. if (preg_match('#^' . $protocol . '/.*?\s+[(404|999)]+\s#i', $headers)) {
  431. return false;
  432. }
  433. return true;
  434. }
  435. /**
  436. * Validation of DateTime Fields (both Date and Time together)
  437. *
  438. * @param options
  439. * - dateFormat (defaults to 'ymd')
  440. * - allowEmpty
  441. * - after/before (fieldName to validate against)
  442. * - min/max (defaults to >= 1 - at least 1 minute apart)
  443. * @return bool Success
  444. */
  445. public function validateDateTime($value, $options = array(), $config = array()) {
  446. $format = !empty($options['dateFormat']) ? $options['dateFormat'] : 'ymd';
  447. $pieces = $value->format(FORMAT_DB_DATETIME);
  448. $dateTime = explode(' ', $pieces, 2);
  449. $date = $dateTime[0];
  450. $time = (!empty($dateTime[1]) ? $dateTime[1] : '');
  451. if (!empty($options['allowEmpty']) && (empty($date) && empty($time) || $date == DEFAULT_DATE && $time == DEFAULT_TIME || $date == DEFAULT_DATE && empty($time))) {
  452. return true;
  453. }
  454. //TODO: cleanup
  455. if (Validation::date($date, $format) && Validation::time($time)) {
  456. // after/before?
  457. $minutes = isset($options['min']) ? $options['min'] : 1;
  458. if (!empty($options['after']) && isset($config['data'][$options['after']])) {
  459. $compare = $value->subMinutes($minutes);
  460. if ($config['data'][$options['after']]->gt($compare)) {
  461. return false;
  462. }
  463. }
  464. if (!empty($options['before']) && isset($config['data'][$options['before']])) {
  465. $compare = $value->addMinutes($minutes);
  466. if ($config['data'][$options['before']]->lt($compare)) {
  467. return false;
  468. }
  469. }
  470. return true;
  471. }
  472. return false;
  473. }
  474. /**
  475. * Validation of Date fields (as the core one is buggy!!!)
  476. *
  477. * @param options
  478. * - dateFormat (defaults to 'ymd')
  479. * - allowEmpty
  480. * - after/before (fieldName to validate against)
  481. * - min (defaults to 0 - equal is OK too)
  482. * @return bool Success
  483. */
  484. public function validateDate($value, $options = array()) {
  485. $format = !empty($options['format']) ? $options['format'] : 'ymd';
  486. $dateTime = explode(' ', $value, 2);
  487. $date = $dateTime[0];
  488. if (!empty($options['allowEmpty']) && (empty($date) || $date == DEFAULT_DATE)) {
  489. return true;
  490. }
  491. if (Validation::date($date, $format)) {
  492. // after/before?
  493. $days = !empty($options['min']) ? $options['min'] : 0;
  494. if (!empty($options['after']) && isset($this->data[$this->alias][$options['after']])) {
  495. if ($this->data[$this->alias][$options['after']] > date(FORMAT_DB_DATE, strtotime($date) - $days * DAY)) {
  496. return false;
  497. }
  498. }
  499. if (!empty($options['before']) && isset($this->data[$this->alias][$options['before']])) {
  500. if ($this->data[$this->alias][$options['before']] < date(FORMAT_DB_DATE, strtotime($date) + $days * DAY)) {
  501. return false;
  502. }
  503. }
  504. return true;
  505. }
  506. return false;
  507. }
  508. /**
  509. * Validation of Time fields
  510. *
  511. * @param array $options
  512. * - timeFormat (defaults to 'hms')
  513. * - allowEmpty
  514. * - after/before (fieldName to validate against)
  515. * - min/max (defaults to >= 1 - at least 1 minute apart)
  516. * @return bool Success
  517. */
  518. public function validateTime($value, $options = array()) {
  519. $dateTime = explode(' ', $value, 2);
  520. $value = array_pop($dateTime);
  521. if (Validation::time($value)) {
  522. // after/before?
  523. if (!empty($options['after']) && isset($this->data[$this->alias][$options['after']])) {
  524. if ($this->data[$this->alias][$options['after']] >= $value) {
  525. return false;
  526. }
  527. }
  528. if (!empty($options['before']) && isset($this->data[$this->alias][$options['before']])) {
  529. if ($this->data[$this->alias][$options['before']] <= $value) {
  530. return false;
  531. }
  532. }
  533. return true;
  534. }
  535. return false;
  536. }
  537. /**
  538. * Validation of Date Fields (>= minDate && <= maxDate)
  539. *
  540. * @param options
  541. * - min/max (TODO!!)
  542. */
  543. public function validateDateRange($value, $options = array()) {
  544. }
  545. /**
  546. * Validation of Time Fields (>= minTime && <= maxTime)
  547. *
  548. * @param options
  549. * - min/max (TODO!!)
  550. */
  551. public function validateTimeRange($value, $options = array()) {
  552. }
  553. }