Table.php 17 KB

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