PageRenderTime 38ms CodeModel.GetById 14ms RepoModel.GetById 0ms app.codeStats 0ms

/Nodes/Model/Node.php

https://github.com/kareypowell/croogo
PHP | 669 lines | 434 code | 65 blank | 170 comment | 49 complexity | 0fb06e1c443e997a30fe106656975320 MD5 | raw file
  1. <?php
  2. App::uses('NodesAppModel', 'Nodes.Model');
  3. /**
  4. * Node
  5. *
  6. * @category Nodes.Model
  7. * @package Croogo.Nodes.Model
  8. * @version 1.0
  9. * @author Fahad Ibnay Heylaal <contact@fahad19.com>
  10. * @license http://www.opensource.org/licenses/mit-license.php The MIT License
  11. * @link http://www.croogo.org
  12. */
  13. class Node extends NodesAppModel {
  14. /**
  15. * Model name
  16. *
  17. * @var string
  18. * @access public
  19. */
  20. public $name = 'Node';
  21. const DEFAULT_TYPE = 'node';
  22. /**
  23. * Publish status
  24. *
  25. * @see PublishableBehavior
  26. * @deprecated Use CroogoStatus::PUBLISHED
  27. */
  28. const STATUS_PUBLISHED = 1;
  29. /**
  30. * Unpublish status
  31. *
  32. * @see PublishableBehavior
  33. * @deprecated Use CroogoStatus::UNPUBLISHED
  34. */
  35. const STATUS_UNPUBLISHED = 0;
  36. /**
  37. * @deprecated Use CroogoStatus::PROMOTED
  38. */
  39. const STATUS_PROMOTED = 1;
  40. /**
  41. * @deprecated Use CroogoStatus::UNPROMOTED
  42. */
  43. const STATUS_UNPROMOTED = 0;
  44. /**
  45. * @deprecated Use BulkProcessBehavior `fields` settings
  46. */
  47. const PUBLICATION_STATE_FIELD = 'status';
  48. /**
  49. * @deprecated Use BulkProcessBehavior `fields` settings
  50. */
  51. const PROMOTION_STATE_FIELD = 'promote';
  52. /**
  53. * @deprecated
  54. */
  55. const UNPROCESSED_ACTION = 'delete';
  56. /**
  57. * Behaviors used by the Model
  58. *
  59. * @var array
  60. * @access public
  61. */
  62. public $actsAs = array(
  63. 'Tree',
  64. 'Croogo.BulkProcess' => array(
  65. 'actionsMap' => array(
  66. 'promote' => 'bulkPromote',
  67. 'unpromote' => 'bulkUnpromote',
  68. ),
  69. ),
  70. 'Croogo.Encoder',
  71. 'Croogo.Publishable',
  72. 'Croogo.Trackable',
  73. 'Meta.Meta',
  74. 'Croogo.Url',
  75. 'Croogo.Cached' => array(
  76. 'groups' => array(
  77. 'nodes',
  78. ),
  79. ),
  80. 'Search.Searchable',
  81. );
  82. /**
  83. * Node type
  84. *
  85. * If the Model is associated to Node model, this variable holds the Node type value
  86. *
  87. * @var string
  88. * @access public
  89. */
  90. public $type = null;
  91. /**
  92. * Guid
  93. *
  94. * @var string
  95. * @access public
  96. */
  97. public $guid = null;
  98. /**
  99. * Validation
  100. *
  101. * @var array
  102. * @access public
  103. */
  104. public $validate = array(
  105. 'title' => array(
  106. 'rule' => 'notEmpty',
  107. 'message' => 'This field cannot be left blank.',
  108. ),
  109. 'slug' => array(
  110. 'isUniquePerType' => array(
  111. 'rule' => 'isUniquePerType',
  112. 'message' => 'This slug has already been taken.',
  113. ),
  114. 'minLength' => array(
  115. 'rule' => array('minLength', 1),
  116. 'message' => 'Slug cannot be empty.',
  117. ),
  118. ),
  119. );
  120. /**
  121. * Filter search fields
  122. *
  123. * @var array
  124. * @access public
  125. */
  126. public $filterArgs = array(
  127. 'q' => array('type' => 'query', 'method' => 'filterPublishedNodes'),
  128. 'filter' => array('type' => 'query', 'method' => 'filterNodes'),
  129. 'title' => array('type' => 'like'),
  130. 'type' => array('type' => 'value'),
  131. 'status' => array('type' => 'value'),
  132. 'promote' => array('type' => 'value'),
  133. );
  134. /**
  135. * Model associations: belongsTo
  136. *
  137. * @var array
  138. * @access public
  139. */
  140. public $belongsTo = array(
  141. 'User' => array(
  142. 'className' => 'Users.User',
  143. 'foreignKey' => 'user_id',
  144. 'conditions' => '',
  145. 'fields' => '',
  146. 'order' => '',
  147. ),
  148. );
  149. public $findMethods = array(
  150. 'promoted' => true,
  151. 'viewBySlug' => true,
  152. 'viewById' => true,
  153. 'published' => true,
  154. );
  155. /**
  156. * beforeFind callback
  157. *
  158. * @param array $q
  159. * @return array
  160. */
  161. public function beforeFind($queryData) {
  162. $typeField = $this->alias . '.type';
  163. if ($this->type != null && !isset($queryData['conditions'][$typeField])) {
  164. $queryData['conditions'][$typeField] = $this->type;
  165. }
  166. return $queryData;
  167. }
  168. /**
  169. * beforeSave callback
  170. *
  171. * @return boolean
  172. */
  173. public function beforeSave($options = array()) {
  174. if (empty($this->data[$this->alias]['type']) && $this->type != null) {
  175. $this->data[$this->alias]['type'] = $this->type;
  176. }
  177. $dateFields = array('created');
  178. foreach ($dateFields as $dateField) {
  179. if (!array_key_exists($dateField, $this->data[$this->alias])) {
  180. continue;
  181. }
  182. if (empty($this->data[$this->alias][$dateField])) {
  183. $db = $this->getDataSource();
  184. $colType = array_merge(array(
  185. 'formatter' => 'date',
  186. ), $db->columns[$this->getColumnType($dateField)]
  187. );
  188. $this->data[$this->alias][$dateField] = call_user_func(
  189. $colType['formatter'], $colType['format']
  190. );
  191. }
  192. }
  193. return true;
  194. }
  195. /**
  196. * Returns false if any fields passed match any (by default, all if $or = false) of their matching values.
  197. *
  198. * @param array $fields Field/value pairs to search (if no values specified, they are pulled from $this->data)
  199. * @param boolean $or If false, all fields specified must match in order for a false return value
  200. * @return boolean False if any records matching any fields are found
  201. * @access public
  202. */
  203. public function isUniquePerType($fields, $or = true) {
  204. if (!is_array($fields)) {
  205. $fields = func_get_args();
  206. if (is_bool($fields[count($fields) - 1])) {
  207. $or = $fields[count($fields) - 1];
  208. unset($fields[count($fields) - 1]);
  209. }
  210. }
  211. foreach ($fields as $field => $value) {
  212. if (is_numeric($field)) {
  213. unset($fields[$field]);
  214. $field = $value;
  215. if (isset($this->data[$this->alias][$field])) {
  216. $value = $this->data[$this->alias][$field];
  217. } else {
  218. $value = null;
  219. }
  220. }
  221. if (strpos($field, '.') === false) {
  222. unset($fields[$field]);
  223. $fields[$this->alias . '.' . $field] = $value;
  224. }
  225. }
  226. if ($or) {
  227. $fields = array('or' => $fields);
  228. }
  229. if (!empty($this->id)) {
  230. $fields[$this->alias . '.' . $this->primaryKey . ' !='] = $this->id;
  231. }
  232. if (!empty($this->type)) {
  233. $fields[$this->alias . '.type'] = $this->type;
  234. }
  235. return ($this->find('count', array('conditions' => $fields, 'recursive' => -1)) == 0);
  236. }
  237. /**
  238. * Return filter condition for Nodes
  239. *
  240. * @return array Array of conditions
  241. */
  242. public function filterNodes($data = array()) {
  243. $conditions = array();
  244. if (!empty($data['filter'])) {
  245. $filter = '%' . $data['filter'] . '%';
  246. $conditions = array(
  247. 'OR' => array(
  248. $this->alias . '.title LIKE' => $filter,
  249. $this->alias . '.excerpt LIKE' => $filter,
  250. $this->alias . '.body LIKE' => $filter,
  251. $this->alias . '.terms LIKE' => $filter,
  252. ),
  253. );
  254. }
  255. return $conditions;
  256. }
  257. /**
  258. * Return filter condition for Nodes
  259. *
  260. * @return array Array of conditions
  261. */
  262. public function filterPublishedNodes($data = array()) {
  263. $conditions = array();
  264. if (!empty($data['filter'])) {
  265. $filter = '%' . $data['filter'] . '%';
  266. $conditions = array(
  267. $this->escapeField('status') => $this->status(),
  268. 'AND' => array(
  269. array(
  270. 'OR' => array(
  271. $this->alias . '.title LIKE' => $filter,
  272. $this->alias . '.excerpt LIKE' => $filter,
  273. $this->alias . '.body LIKE' => $filter,
  274. $this->alias . '.terms LIKE' => $filter,
  275. ),
  276. ),
  277. array(
  278. $visibilityRolesField => '',
  279. $visibilityRolesField . ' LIKE' => '%"' . $this->Croogo->roleId() . '"%',
  280. ),
  281. ),
  282. );
  283. }
  284. return $conditions;
  285. }
  286. /**
  287. * Create/update a Node record
  288. *
  289. * @param $data array Node data
  290. * @param $typeAlias string Node type alias
  291. * @return mixed see Model::saveAll()
  292. */
  293. public function saveNode($data, $typeAlias = self::DEFAULT_TYPE) {
  294. $result = false;
  295. $data = $this->formatData($data, $typeAlias);
  296. $event = Croogo::dispatchEvent('Model.Node.beforeSaveNode', $this, compact('data', 'typeAlias'));
  297. $result = $this->saveAll($event->data['data']);
  298. Croogo::dispatchEvent('Model.Node.afterSaveNode', $this, $event->data);
  299. return $result;
  300. }
  301. /**
  302. * Format data for saving
  303. *
  304. * @param array $data Node and related data, eg Taxonomy and Role
  305. * @param string $typeAlias string Node type alias
  306. * @return array formatted data
  307. * @throws InvalidArgumentException
  308. */
  309. public function formatData($data, $typeAlias = self::DEFAULT_TYPE) {
  310. $roles = $type = array();
  311. if (!array_key_exists($this->alias, $data)) {
  312. $data = array($this->alias => $data);
  313. } else {
  314. $data = $data;
  315. }
  316. if (empty($data[$this->alias]['path'])) {
  317. $data[$this->alias]['path'] = $this->_getNodeRelativePath($data);
  318. }
  319. if (!array_key_exists('Role', $data) || empty($data['Role']['Role'])) {
  320. $roles = '';
  321. } else {
  322. $roles = $data['Role']['Role'];
  323. }
  324. $data[$this->alias]['visibility_roles'] = $this->encodeData($roles);
  325. return $data;
  326. }
  327. /**
  328. * Update values for all nodes 'path' field
  329. *
  330. * @return bool|array Depending on atomicity
  331. * @see Model::saveMany()
  332. */
  333. public function updateAllNodesPaths() {
  334. $types = $this->Taxonomy->Vocabulary->Type->find('list', array(
  335. 'fields' => array(
  336. 'Type.id',
  337. 'Type.alias',
  338. ),
  339. ));
  340. $typesAlias = array_values($types);
  341. $idField = $this->escapeField();
  342. $batch = 30;
  343. $options = array(
  344. 'order' => $idField,
  345. 'conditions' => array(
  346. $this->alias . '.type' => $typesAlias,
  347. ),
  348. 'fields' => array(
  349. $this->alias . '.id',
  350. $this->alias . '.slug',
  351. $this->alias . '.type',
  352. $this->alias . '.path',
  353. ),
  354. 'recursive' => '-1',
  355. 'limit' => $batch,
  356. );
  357. $results = array();
  358. while ($nodes = $this->find('all', $options)) {
  359. foreach ($nodes as &$node) {
  360. $node[$this->alias]['path'] = $this->_getNodeRelativePath($node);
  361. }
  362. $result = $this->saveMany($nodes, array('fieldList' => array('path')));
  363. if ($result === false) {
  364. $this->log('updateAllNodesPath batch failed:');
  365. $this->log($this->validationErrors);
  366. return false;
  367. }
  368. $results[] = $result;
  369. $options['conditions'][$idField . ' >'] = $node[$this->alias]['id'];
  370. if (count($nodes) < $batch) {
  371. break;
  372. }
  373. }
  374. return $this->saveMany($nodes, array('fieldList' => array('path')));
  375. }
  376. /**
  377. * getNodeRelativePath
  378. *
  379. * @param array $node Node array
  380. * @return string relative node path
  381. */
  382. protected function _getNodeRelativePath($node) {
  383. return Croogo::getRelativePath(array(
  384. 'plugin' => 'nodes',
  385. 'admin' => false,
  386. 'controller' => 'nodes',
  387. 'action' => 'view',
  388. 'type' => $this->_getType($node),
  389. 'slug' => $node[$this->alias]['slug'],
  390. ));
  391. }
  392. /**
  393. * _getType
  394. *
  395. * @param array $data Node data
  396. * @return string type
  397. */
  398. protected function _getType($data) {
  399. if (empty($data[$this->alias]['type'])) {
  400. $type = is_null($this->type) ? self::DEFAULT_TYPE : $this->type;
  401. } else {
  402. $type = $data[$this->alias]['type'];
  403. }
  404. return $type;
  405. }
  406. /**
  407. * Find promoted nodes
  408. *
  409. * @see Model::find()
  410. * @see Model::_findAll()
  411. */
  412. protected function _findPromoted($state, $query, $results = array()) {
  413. if ($state === 'before') {
  414. $_defaultFilters = array('contain', 'limit', 'order', 'conditions');
  415. $_defaultContain = array(
  416. 'Meta',
  417. 'Taxonomy' => array(
  418. 'Term',
  419. 'Vocabulary',
  420. ),
  421. 'User',
  422. );
  423. $_defaultConditions = array(
  424. $this->escapeField('status') => $this->status(),
  425. $this->escapeField('promote') => self::STATUS_PROMOTED,
  426. 'OR' => array(
  427. $this->escapeField('visibility_roles') => '',
  428. ),
  429. );
  430. $_defaultOrder = $this->escapeField('created') . ' DESC';
  431. $_defaultLimit = Configure::read('Reading.nodes_per_page');
  432. foreach ($_defaultFilters as $filter) {
  433. $this->_mergeQueryFilters($query, $filter, ${'_default' . ucfirst($filter)});
  434. }
  435. return $query;
  436. } else {
  437. return $results;
  438. }
  439. }
  440. /**
  441. * Find a single node by id
  442. */
  443. protected function _findViewById($state, $query, $results = array()) {
  444. if ($state == 'after') {
  445. if (isset($results[0])) {
  446. return $results[0];
  447. }
  448. return $results;
  449. }
  450. if ($query['conditions'] === null) {
  451. $query = Hash::merge($query, array(
  452. 'conditions' => array(),
  453. ));
  454. }
  455. $keys = array('id' => null, 'roleId' => null);
  456. $args = array_merge($keys, array_intersect_key($query, $keys));
  457. $query = array_diff_key($query, $args);
  458. $visibilityRolesField = $this->escapeField('visibility_roles');
  459. $query = Hash::merge(array(
  460. 'conditions' => array(
  461. $this->escapeField() => $args['id'],
  462. $this->escapeField('status') => $this->status(),
  463. 'OR' => array(
  464. $visibilityRolesField => '',
  465. $visibilityRolesField . ' LIKE' => '%"' . $args['roleId'] . '"%',
  466. ),
  467. ),
  468. 'contain' => array(
  469. 'Meta',
  470. 'Taxonomy' => array(
  471. 'Term',
  472. 'Vocabulary',
  473. ),
  474. 'User',
  475. ),
  476. 'cache' => array(
  477. 'name' => 'node_' . $args['roleId'] . '_' . $args['id'],
  478. 'config' => 'nodes_view',
  479. ),
  480. ), $query);
  481. return $query;
  482. }
  483. /**
  484. * Find a single node by slug
  485. */
  486. protected function _findViewBySlug($state, $query, $results = array()) {
  487. if ($state == 'after') {
  488. if (isset($results[0])) {
  489. return $results[0];
  490. }
  491. return $results;
  492. }
  493. if ($query['conditions'] === null) {
  494. $query = Hash::merge($query, array(
  495. 'conditions' => array(),
  496. ));
  497. }
  498. $keys = array('slug' => null, 'type' => null, 'roleId' => null);
  499. $args = array_merge($keys, array_intersect_key($query, $keys));
  500. $query = array_diff_key($query, $args);
  501. $visibilityRolesField = $this->escapeField('visibility_roles');
  502. $query = Hash::merge(array(
  503. 'conditions' => array(
  504. $this->escapeField('slug') => $args['slug'],
  505. $this->escapeField('type') => $args['type'],
  506. $this->escapeField('status') => $this->status(),
  507. 'OR' => array(
  508. $visibilityRolesField => '',
  509. $visibilityRolesField . ' LIKE' => '%"' . $args['roleId'] . '"%',
  510. ),
  511. ),
  512. 'contain' => array(
  513. 'Meta',
  514. 'Taxonomy' => array(
  515. 'Term',
  516. 'Vocabulary',
  517. ),
  518. 'User',
  519. ),
  520. 'cache' => array(
  521. 'name' => 'node_' . $args['roleId'] . '_' . $args['type'] . '_' . $args['slug'],
  522. 'config' => 'nodes_view',
  523. ),
  524. ), $query);
  525. return $query;
  526. }
  527. /**
  528. * Search published nodes
  529. *
  530. * $query options:
  531. *
  532. * - `q`: term to search
  533. * - `roleId`: Role Id
  534. * - `typeAlias`: Type alias
  535. */
  536. protected function _findPublished($state, $query, $results = array()) {
  537. if ($state == 'after') {
  538. return $results;
  539. }
  540. $q = isset($query['q']) ? $query['q'] : null;
  541. $like = empty($q) ? '%' : '%' . $q . '%';
  542. $roleId = isset($query['roleId']) ? $query['roleId'] : null;
  543. $typeAlias = isset($query['typeAlias']) ? $query['typeAlias'] : null;
  544. $visibilityRolesField = $this->escapeField('visibility_roles');
  545. $nodeOrConditions = array();
  546. if ($like) {
  547. $nodeOrConditions = array_merge($nodeOrConditions, array(
  548. $this->escapeField('title') . ' LIKE' => $like,
  549. $this->escapeField('excerpt') . ' LIKE' => $like,
  550. $this->escapeField('body') . ' LIKE' => $like,
  551. $this->escapeField('terms') . ' LIKE' => $like,
  552. ));
  553. }
  554. $defaults = array(
  555. 'order' => $this->escapeField('created') . ' DESC',
  556. 'limit' => Configure::read('Reading.nodes_per_page'),
  557. 'conditions' => array(
  558. $this->escapeField('status') => $this->status(),
  559. 'AND' => array(
  560. array(
  561. 'OR' => $nodeOrConditions,
  562. ),
  563. array(
  564. 'OR' => array(
  565. $visibilityRolesField => '',
  566. $visibilityRolesField . ' LIKE' => '%"' . $roleId . '"%',
  567. ),
  568. ),
  569. ),
  570. ),
  571. 'contain' => array(
  572. 'Meta',
  573. 'Taxonomy' => array(
  574. 'Term',
  575. 'Vocabulary',
  576. ),
  577. 'User',
  578. ),
  579. );
  580. if (isset($typeAlias)) {
  581. $defaults['conditions'][$this->escapeField('type')] = $typeAlias;
  582. }
  583. if (empty($query['conditions'])) {
  584. $query['conditions'] = array();
  585. }
  586. $query = Hash::merge($defaults, $query);
  587. return $query;
  588. }
  589. /**
  590. * mergeQueryFilters
  591. *
  592. * @see Node::_findPromoted()
  593. * @return void
  594. */
  595. protected function _mergeQueryFilters(&$query, $key, $values) {
  596. if (!empty($query[$key])) {
  597. if (is_array($query[$key])) {
  598. $query[$key] = Hash::merge($query[$key], $values);
  599. }
  600. } else {
  601. $query[$key] = $values;
  602. }
  603. }
  604. }