Table.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482
  1. <?php
  2. namespace Tools\Model\Table;
  3. use Cake\Core\Configure;
  4. use Shim\Model\Table\Table as ShimTable;
  5. use Cake\Utility\Inflector;
  6. use Cake\Validation\Validation;
  7. use Cake\Validation\Validator;
  8. use Tools\Utility\Utility;
  9. use Cake\ORM\Query;
  10. use Cake\Event\Event;
  11. use Tools\Utility\Time;
  12. use Cake\Routing\Router;
  13. class Table extends ShimTable {
  14. /**
  15. * @param array $entities
  16. * @return bool
  17. */
  18. public function validateAll(array $entities) {
  19. foreach ($entities as $entity) {
  20. if ($entity->errors()) {
  21. return false;
  22. }
  23. }
  24. return true;
  25. }
  26. /**
  27. * @param array $entities
  28. * @return bool
  29. */
  30. public function saveAll(array $entities) {
  31. $result = true;
  32. foreach ($entities as $entity) {
  33. if (!$this->save($entity)) {
  34. $result = false;
  35. }
  36. }
  37. return $result;
  38. }
  39. /**
  40. * Validator method used to check the uniqueness of a value for a column.
  41. * This is meant to be used with the validation API and not to be called
  42. * directly.
  43. *
  44. * ### Example:
  45. *
  46. * {{{
  47. * $validator->add('email', [
  48. * 'unique' => ['rule' => 'validateUnique', 'provider' => 'table']
  49. * ])
  50. * }}}
  51. *
  52. * Unique validation can be scoped to the value of another column:
  53. *
  54. * {{{
  55. * $validator->add('email', [
  56. * 'unique' => [
  57. * 'rule' => ['validateUnique', ['scope' => 'site_id']],
  58. * 'provider' => 'table'
  59. * ]
  60. * ]);
  61. * }}}
  62. *
  63. * In the above example, the email uniqueness will be scoped to only rows having
  64. * the same site_id. Scoping will only be used if the scoping field is present in
  65. * the data to be validated.
  66. *
  67. * @override To allow multiple scoped values
  68. *
  69. * @param mixed $value The value of column to be checked for uniqueness
  70. * @param array $options The options array, optionally containing the 'scope' key
  71. * @param array $context The validation context as provided by the validation routine
  72. * @return bool true if the value is unique
  73. */
  74. public function validateUniqueExt($value, array $options, array $context = []) {
  75. $context += $options;
  76. return parent::validateUnique($value, $context);
  77. }
  78. /**
  79. * Checks a record, if it is unique - depending on other fields in this table (transfered as array)
  80. * example in model: 'rule' => array ('validateUnique', array('belongs_to_table_id','some_id','user_id')),
  81. * if all keys (of the array transferred) match a record, return false, otherwise true
  82. *
  83. * @param array $fields Other fields to depend on
  84. * TODO: add possibity of deep nested validation (User -> Comment -> CommentCategory: UNIQUE comment_id, Comment.user_id)
  85. * @param array $options
  86. * - requireDependentFields Require all dependent fields for the validation rule to return true
  87. * @return bool Success
  88. * @deprecated NOT WORKING anymore, use core validateUnique
  89. */
  90. public function validateUniqueOld($fieldValue, $fields = [], $options = []) {
  91. $id = (!empty($this->data[$this->primaryKey]) ? $this->data[$this->primaryKey] : 0);
  92. if (!$id && $this->id) {
  93. $id = $this->id;
  94. }
  95. $conditions = [
  96. $this->alias . '.' . $fieldName => $fieldValue,
  97. $this->alias . '.id !=' => $id];
  98. $fields = (array)$fields;
  99. if (!array_key_exists('allowEmpty', $fields)) {
  100. foreach ($fields as $dependingField) {
  101. if (isset($this->data[$dependingField])) { // add ONLY if some content is transfered (check on that first!)
  102. $conditions[$this->alias . '.' . $dependingField] = $this->data[$dependingField];
  103. } elseif (isset($this->data['Validation'][$dependingField])) { // add ONLY if some content is transfered (check on that first!
  104. $conditions[$this->alias . '.' . $dependingField] = $this->data['Validation'][$dependingField];
  105. } elseif (!empty($id)) {
  106. // manual query! (only possible on edit)
  107. $res = $this->find('first', ['fields' => [$this->alias . '.' . $dependingField], 'conditions' => [$this->alias . '.id' => $id]]);
  108. if (!empty($res)) {
  109. $conditions[$this->alias . '.' . $dependingField] = $res[$dependingField];
  110. }
  111. } else {
  112. if (!empty($options['requireDependentFields'])) {
  113. trigger_error('Required field ' . $dependingField . ' for validateUnique validation not present');
  114. return false;
  115. }
  116. return true;
  117. }
  118. }
  119. }
  120. $this->recursive = -1;
  121. if (count($conditions) > 2) {
  122. $this->recursive = 0;
  123. }
  124. $options = ['fields' => [$this->alias . '.' . $this->primaryKey], 'conditions' => $conditions];
  125. $res = $this->find('first', $options);
  126. return empty($res);
  127. }
  128. /**
  129. * Return the next auto increment id from the current table
  130. * UUIDs will return false
  131. *
  132. * @return int|bool next auto increment value or False on failure
  133. */
  134. public function getNextAutoIncrement() {
  135. $query = "SHOW TABLE STATUS WHERE name = '" . $this->table() . "'";
  136. $statement = $this->_connection->execute($query);
  137. $result = $statement->fetch();
  138. if (!isset($result[10])) {
  139. return false;
  140. }
  141. return (int)$result[10];
  142. }
  143. /**
  144. * truncate()
  145. *
  146. * @return void
  147. */
  148. public function truncate() {
  149. $sql = $this->schema()->truncateSql($this->_connection);
  150. foreach ($sql as $snippet) {
  151. $this->_connection->execute($snippet);
  152. }
  153. }
  154. /**
  155. * Get all related entries that have been used so far
  156. *
  157. * @param string $tableName The related model
  158. * @param string $groupField Field to group by
  159. * @param string $type Find type
  160. * @param array $options
  161. * @return array
  162. */
  163. public function getRelatedInUse($tableName, $groupField = null, $type = 'all', $options = []) {
  164. if ($groupField === null) {
  165. $groupField = $this->belongsTo[$tableName]['foreignKey'];
  166. }
  167. $defaults = [
  168. 'contain' => [$tableName],
  169. 'group' => $groupField,
  170. 'order' => isset($this->$tableName->order) ? $this->$tableName->order : [$tableName . '.' . $this->$tableName->displayField() => 'ASC'],
  171. ];
  172. if ($type === 'list') {
  173. $defaults['fields'] = [$tableName . '.' . $this->$tableName->primaryKey(), $tableName . '.' . $this->$tableName->displayField()];
  174. }
  175. $options += $defaults;
  176. return $this->find($type, $options);
  177. }
  178. /**
  179. * Get all fields that have been used so far
  180. *
  181. * @param string $groupField Field to group by
  182. * @param string $type Find type
  183. * @param array $options
  184. * @return array
  185. */
  186. public function getFieldInUse($groupField, $type = 'all', $options = []) {
  187. $defaults = [
  188. 'group' => $groupField,
  189. 'order' => [$this->alias . '.' . $this->displayField => 'ASC'],
  190. ];
  191. if ($type === 'list') {
  192. $defaults['fields'] = [$this->alias . '.' . $this->primaryKey, $this->alias . '.' . $this->displayField];
  193. }
  194. $options += $defaults;
  195. return $this->find($type, $options);
  196. }
  197. /**
  198. * Checks if the content of 2 fields are equal
  199. * Does not check on empty fields! Return TRUE even if both are empty (secure against empty in another rule)!
  200. *
  201. * Options:
  202. * - compare: field to compare to
  203. * - cast: if casting should be applied to both values
  204. *
  205. * @param mixed $value
  206. * @param array $options
  207. * @return bool Success
  208. */
  209. public function validateIdentical($value, $options = [], array $context = []) {
  210. if (!is_array($options)) {
  211. $options = ['compare' => $options];
  212. }
  213. if (!isset($context['data'][$options['compare']])) {
  214. return false;
  215. }
  216. $compareValue = $context['data'][$options['compare']];
  217. $matching = ['string' => 'string', 'int' => 'integer', 'float' => 'float', 'bool' => 'boolean'];
  218. if (!empty($options['cast']) && array_key_exists($options['cast'], $matching)) {
  219. // cast values to string/int/float/bool if desired
  220. settype($compareValue, $matching[$options['cast']]);
  221. settype($value, $matching[$options['cast']]);
  222. }
  223. return ($compareValue === $value);
  224. }
  225. /**
  226. * Checks if a url is valid AND accessable (returns false otherwise)
  227. *
  228. * @param array/string $data: full url(!) starting with http://...
  229. * @options array
  230. * - allowEmpty TRUE/FALSE (TRUE: if empty => return TRUE)
  231. * - required TRUE/FALSE (TRUE: overrides allowEmpty)
  232. * - autoComplete (default: TRUE)
  233. * - deep (default: TRUE)
  234. * @return bool Success
  235. */
  236. public function validateUrl($url, $options = [], array $context = []) {
  237. if (empty($url)) {
  238. if (!empty($options['allowEmpty']) && empty($options['required'])) {
  239. return true;
  240. }
  241. return false;
  242. }
  243. if (!isset($options['autoComplete']) || $options['autoComplete'] !== false) {
  244. $url = $this->_autoCompleteUrl($url);
  245. }
  246. if (!isset($options['strict']) || $options['strict'] !== false) {
  247. $options['strict'] = true;
  248. }
  249. // validation
  250. if (!Validation::url($url, $options['strict']) && env('REMOTE_ADDR') && env('REMOTE_ADDR') !== '127.0.0.1') {
  251. return false;
  252. }
  253. // same domain?
  254. if (!empty($options['sameDomain']) && env('HTTP_HOST')) {
  255. $is = parse_url($url, PHP_URL_HOST);
  256. $expected = env('HTTP_HOST');
  257. if (mb_strtolower($is) !== mb_strtolower($expected)) {
  258. return false;
  259. }
  260. }
  261. if (isset($options['deep']) && $options['deep'] === false) {
  262. return true;
  263. }
  264. return $this->_validUrl($url);
  265. }
  266. /**
  267. * Prepend protocol if missing
  268. *
  269. * @param string $url
  270. * @return string Url
  271. */
  272. protected function _autoCompleteUrl($url) {
  273. if (mb_strpos($url, '/') === 0) {
  274. $url = Router::url($url, true);
  275. } elseif (mb_strpos($url, '://') === false && mb_strpos($url, 'www.') === 0) {
  276. $url = 'http://' . $url;
  277. }
  278. return $url;
  279. }
  280. /**
  281. * Checks if a url is valid
  282. *
  283. * @param string url
  284. * @return bool Success
  285. */
  286. protected function _validUrl($url) {
  287. $headers = Utility::getHeaderFromUrl($url);
  288. if ($headers === false) {
  289. return false;
  290. }
  291. $headers = implode("\n", $headers);
  292. $protocol = mb_strpos($url, 'https://') === 0 ? 'HTTP' : 'HTTP';
  293. if (!preg_match('#^' . $protocol . '/.*?\s+[(200|301|302)]+\s#i', $headers)) {
  294. return false;
  295. }
  296. if (preg_match('#^' . $protocol . '/.*?\s+[(404|999)]+\s#i', $headers)) {
  297. return false;
  298. }
  299. return true;
  300. }
  301. /**
  302. * Validation of DateTime Fields (both Date and Time together)
  303. *
  304. * @param options
  305. * - dateFormat (defaults to 'ymd')
  306. * - allowEmpty
  307. * - after/before (fieldName to validate against)
  308. * - min/max (defaults to >= 1 - at least 1 minute apart)
  309. * @return bool Success
  310. */
  311. public function validateDateTime($value, $options = [], array $context = []) {
  312. if (!$value) {
  313. if (!empty($options['allowEmpty'])) {
  314. return true;
  315. }
  316. return false;
  317. }
  318. $format = !empty($options['dateFormat']) ? $options['dateFormat'] : 'ymd';
  319. if (!is_object($value)) {
  320. $value = new Time($value);
  321. }
  322. $pieces = $value->format(FORMAT_DB_DATETIME);
  323. $dateTime = explode(' ', $pieces, 2);
  324. $date = $dateTime[0];
  325. $time = (!empty($dateTime[1]) ? $dateTime[1] : '');
  326. if (!empty($options['allowEmpty']) && (empty($date) && empty($time) || $date === DEFAULT_DATE && $time === DEFAULT_TIME || $date === DEFAULT_DATE && empty($time))) {
  327. return true;
  328. }
  329. //TODO: cleanup
  330. if (Validation::date($date, $format) && Validation::time($time)) {
  331. // after/before?
  332. $seconds = isset($options['min']) ? $options['min'] : 1;
  333. if (!empty($options['after']) && isset($context['data'][$options['after']])) {
  334. $compare = $value->subSeconds($seconds);
  335. if (!is_object($context['data'][$options['after']])) {
  336. $context['data'][$options['after']] = new Time($context['data'][$options['after']]);
  337. }
  338. if ($context['data'][$options['after']]->gt($compare)) {
  339. return false;
  340. }
  341. }
  342. if (!empty($options['before']) && isset($context['data'][$options['before']])) {
  343. $compare = $value->addSeconds($seconds);
  344. if (!is_object($context['data'][$options['before']])) {
  345. $context['data'][$options['before']] = new Time($context['data'][$options['before']]);
  346. }
  347. if ($context['data'][$options['before']]->lt($compare)) {
  348. return false;
  349. }
  350. }
  351. return true;
  352. }
  353. return false;
  354. }
  355. /**
  356. * Validation of Date fields (as the core one is buggy!!!)
  357. *
  358. * @param options
  359. * - dateFormat (defaults to 'ymd')
  360. * - allowEmpty
  361. * - after/before (fieldName to validate against)
  362. * - min (defaults to 0 - equal is OK too)
  363. * @return bool Success
  364. */
  365. public function validateDate($value, $options = [], array $context = []) {
  366. if (!$value) {
  367. if (!empty($options['allowEmpty'])) {
  368. return true;
  369. }
  370. return false;
  371. }
  372. $format = !empty($options['format']) ? $options['format'] : 'ymd';
  373. if (!is_object($value)) {
  374. $value = new Time($value);
  375. }
  376. $date = $value->format(FORMAT_DB_DATE);
  377. if (!empty($options['allowEmpty']) && (empty($date) || $date == DEFAULT_DATE)) {
  378. return true;
  379. }
  380. if (Validation::date($date, $format)) {
  381. // after/before?
  382. $days = !empty($options['min']) ? $options['min'] : 0;
  383. if (!empty($options['after']) && isset($context['data'][$options['after']])) {
  384. $compare = $value->subDays($days);
  385. if ($context['data'][$options['after']]->gt($compare)) {
  386. return false;
  387. }
  388. }
  389. if (!empty($options['before']) && isset($context['data'][$options['before']])) {
  390. $compare = $value->addDays($days);
  391. if ($context['data'][$options['before']]->lt($compare)) {
  392. return false;
  393. }
  394. }
  395. return true;
  396. }
  397. return false;
  398. }
  399. /**
  400. * Validation of Time fields
  401. *
  402. * @param array $options
  403. * - timeFormat (defaults to 'hms')
  404. * - allowEmpty
  405. * - after/before (fieldName to validate against)
  406. * - min/max (defaults to >= 1 - at least 1 minute apart)
  407. * @return bool Success
  408. */
  409. public function validateTime($value, $options = [], array $context = []) {
  410. if (!$value) {
  411. return false;
  412. }
  413. $dateTime = explode(' ', $value, 2);
  414. $value = array_pop($dateTime);
  415. if (Validation::time($value)) {
  416. // after/before?
  417. if (!empty($options['after']) && isset($context['data'][$options['after']])) {
  418. if ($context['data'][$options['after']] >= $value) {
  419. return false;
  420. }
  421. }
  422. if (!empty($options['before']) && isset($context['data'][$options['before']])) {
  423. if ($context['data'][$options['before']] <= $value) {
  424. return false;
  425. }
  426. }
  427. return true;
  428. }
  429. return false;
  430. }
  431. /**
  432. * Validation of Date Fields (>= minDate && <= maxDate)
  433. *
  434. * @param options
  435. * - min/max (TODO!!)
  436. */
  437. public function validateDateRange($value, $options = [], array $context = []) {
  438. }
  439. /**
  440. * Validation of Time Fields (>= minTime && <= maxTime)
  441. *
  442. * @param options
  443. * - min/max (TODO!!)
  444. */
  445. public function validateTimeRange($value, $options = [], array $context = []) {
  446. }
  447. }