1 <?php
2
3 /*
4 * This file is part of the Icybee package.
5 *
6 * (c) Olivier Laviale <olivier.laviale@gmail.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12 namespace Icybee\Modules\Pages;
13
14 use ICanBoogie\ActiveRecord\Query;
15 use ICanBoogie\Exception;
16 use ICanBoogie\Routing\Pattern;
17
18 class Model extends \Icybee\Modules\Nodes\Model
19 {
20 /**
21 * Before saving the record, we make sure that it is not its own parent.
22 */
23 public function save(array $properties, $key=null, array $options=array())
24 {
25 if ($key && isset($properties[Page::PARENTID]) && $key == $properties[Page::PARENTID])
26 {
27 throw new Exception('A page connot be its own parent.');
28 }
29
30 if (empty($properties[Page::SITEID]))
31 {
32 throw new Exception('site_id is empty.');
33 }
34
35 unset(self::$blueprint_cache[$properties[Page::SITEID]]);
36
37 return parent::save($properties, $key, $options);
38 }
39
40 /**
41 * Before deleting the record, we make sure that it is not used as a parent page or as a
42 * location target.
43 */
44 public function delete($key)
45 {
46 $site_id = $this->select('siteid')->filter_by_nid($key)->rc;
47
48 if ($site_id)
49 {
50 unset(self::$blueprint_cache[$site_id]);
51 }
52
53 return parent::delete($key);
54 }
55
56 /**
57 * Changes the order of the query with "weight, create".
58 *
59 * @param Query $query
60 *
61 * @return Query
62 */
63 protected function scope_ordered(Query $query, $direction=1)
64 {
65 $direction = $direction < 0 ? 'DESC' : 'ASC';
66
67 return $query->order("weight {$direction}, created_at {$direction}");
68 }
69
70 /**
71 * Returns the blueprint of the pages tree.
72 *
73 * @param int $site_id Identifier of the website.
74 *
75 * @return array[int]object
76 */
77 public function blueprint($site_id)
78 {
79 if (isset(self::$blueprint_cache[$site_id]))
80 {
81 return self::$blueprint_cache[$site_id];
82 }
83
84 $query = $this
85 ->select('nid, parentid, is_online, is_navigation_excluded, pattern')
86 ->filter_by_siteid($site_id)->ordered;
87
88 return self::$blueprint_cache[$site_id] = Blueprint::from($query);
89 }
90
91 /**
92 * Holds the cached blueprint for each website.
93 *
94 * @var array
95 */
96 private static $blueprint_cache = array();
97
98 /**
99 * Returns the home page of the specified site.
100 *
101 * The record cache is used to retrieve or store the home page. Additionnaly the home page
102 * found is stored for each site.
103 *
104 * @param int $siteid Identifier of the site.
105 *
106 * @return Page
107 */
108 public function find_home($siteid)
109 {
110 if (isset(self::$home_by_siteid[$siteid]))
111 {
112 return self::$home_by_siteid[$siteid];
113 }
114
115 $home = $this->where('siteid = ? AND parentid = 0 AND is_online = 1', $siteid)->ordered->one;
116
117 if ($home)
118 {
119 $stored = $this->retrieve($home->nid);
120
121 if ($stored)
122 {
123 $home = $stored;
124 }
125 else
126 {
127 $this->store($home);
128 }
129 }
130
131 self::$home_by_siteid[$siteid] = $home;
132
133 return $home;
134 }
135
136 private static $home_by_siteid = array();
137
138 /**
139 * Finds a page using its path.
140 *
141 * @param string $path
142 *
143 * @return Page
144 */
145 public function find_by_path($path) // TODO-20120922: use a BluePrint object
146 {
147 global $core;
148
149 $pos = strrpos($path, '.');
150 $extension = null;
151
152 if ($pos && $pos > strrpos($path, '/'))
153 {
154 $extension = substr($path, $pos);
155 $path = substr($path, 0, $pos);
156 }
157
158 $l = strlen($path);
159
160 if ($l && $path{$l - 1} == '/')
161 {
162 $path = substr($path, 0, -1);
163 }
164
165 #
166 # matching site
167 #
168
169 $site = $core->site;
170 $siteid = $site->siteid;
171 $site_path = $site->path;
172
173 if ($site_path)
174 {
175 if (strpos($path, $site_path) !== 0)
176 {
177 return;
178 }
179
180 $path = substr($path, strlen($site_path));
181 }
182
183 if (!$path)
184 {
185 #
186 # The home page is requested, we load the first parentless online page of the site.
187 #
188
189 $page = $this->find_home($siteid);
190
191 if (!$page)
192 {
193 return;
194 }
195
196 if (!$this->retrieve($page->nid))
197 {
198 $this->store($page);
199 }
200
201 return $page;
202 }
203
204 $parts = explode('/', $path);
205
206 array_shift($parts);
207
208 $parts_n = count($parts);
209
210 $vars = array();
211
212 #
213 # We load from all the pages just what we need to find a matching path, and create a tree
214 # with it.
215 #
216
217 $tries = $this->select('nid, parentid, slug, pattern')->filter_by_siteid($siteid)->ordered->all(\PDO::FETCH_OBJ);
218 $tries = self::nestNodes($tries);
219
220 $try = null;
221 $pages_by_ids = array();
222
223 for ($i = 0 ; $i < $parts_n ; $i++)
224 {
225 if ($try)
226 {
227 $tries = $try->children;
228 }
229
230 $part = $path_part = $parts[$i];
231
232 #
233 # first we search for a matching slug
234 #
235
236 foreach ($tries as $try)
237 {
238 $pattern = $try->pattern;
239
240 if ($pattern)
241 {
242 $stripped = preg_replace('#<[^>]+>#', '', $pattern);
243 $nparts = substr_count($stripped, '/') + 1;
244 $path_part = implode('/', array_slice($parts, $i, $nparts));
245
246 $pattern = Pattern::from($pattern);
247
248 if (!$pattern->match($path_part, $path_captured))
249 {
250 $try = null;
251
252 continue;
253 }
254
255 #
256 # found matching pattern !
257 # we skip parts ate by the pattern
258 #
259
260 $i += $nparts - 1;
261
262 #
263 # even if the pattern matched, $match is not guaranteed to be an array,
264 # 'feed.xml' is a valid pattern. // FIXME-20110327: is it still ?
265 #
266
267 if (is_array($path_captured))
268 {
269 $vars = $path_captured + $vars;
270 }
271
272 break;
273 }
274 else if ($part == $try->slug)
275 {
276 break;
277 }
278
279 $try = null;
280 }
281
282 #
283 # If `try` is null at this point it's that the path could not be matched.
284 #
285
286 if (!$try)
287 {
288 return;
289 }
290
291 #
292 # otherwise, we continue
293 #
294
295 $pages_by_ids[$try->nid] = array
296 (
297 'url_part' => $path_part,
298 'url_variables' => $vars
299 );
300 }
301
302 #
303 # append the extension (if any) to the last page of the branch
304 #
305
306 $pages_by_ids[$try->nid]['url_part'] .= $extension;
307
308 #
309 # All page objects have been loaded, we need to set up some additionnal properties, link
310 # each page to its parent and propagate the online status.
311 #
312
313 $parent = null;
314 $pages = $this->find(array_keys($pages_by_ids));
315
316 foreach ($pages as $page)
317 {
318 $page->url_part = $pages_by_ids[$page->nid]['url_part'];
319 $page->url_variables = $pages_by_ids[$page->nid]['url_variables'];
320
321 if ($parent)
322 {
323 // $page->parent = $parent;
324
325 if (!$parent->is_online)
326 {
327 $page->is_online = false;
328 }
329 }
330
331 $parent = $page;
332 }
333
334 return $page;
335 }
336
337 /**
338 * Nest an array of nodes, using their `parentid` property.
339 *
340 * Children are stored in the `children` property of their parents.
341 *
342 * Parent is stored in the `parent` property of its children.
343 *
344 * @param array $entries The array of nodes.
345 * @param array $parents The array of nodes, where the key is the entry's `nid`.
346 */
347 static public function nestNodes($entries, &$entries_by_ids=null) // TODO-20120922: deprecate
348 {
349 #
350 # In order to easily access entries, they are store by their Id in an array.
351 #
352
353 $entries_by_ids = array();
354
355 foreach ($entries as $entry)
356 {
357 $entry->children = array();
358
359 $entries_by_ids[$entry->nid] = $entry;
360 }
361
362 #
363 #
364 #
365
366 $tree = array();
367
368 foreach ($entries_by_ids as $entry)
369 {
370 if (!$entry->parentid || empty($entries_by_ids[$entry->parentid]))
371 {
372 $tree[] = $entry;
373
374 continue;
375 }
376
377 $entry->parent = $entries_by_ids[$entry->parentid];
378 $entry->parent->children[] = $entry;
379 }
380
381 return $tree;
382 }
383
384 /**
385 * Walk the nodes and sets their depth level.
386 *
387 * @param $nodes The nodes to walk through.
388 * @param $max_depth The maximum depth level of the nodes. Nodes beyond the max_depth are removed.
389 * Default to false (no maximum depth level).
390 * @param $depth The depth level to start from. Default to 0.
391 */
392 static public function setNodesDepth($nodes, $max_depth=false, $depth=0) // TODO-20120922: deprecate
393 {
394 foreach ($nodes as $node)
395 {
396 $node->depth = $depth;
397
398 if ($node->children)
399 {
400 if ($max_depth !== false && $max_depth == $depth)
401 {
402 if ($max_depth === 1)
403 {
404 echo "<h1>max_depth ($max_depth) reached for</h1>";
405 var_dump($node);
406 }
407
408 #
409 # The `children` property is unset rather then emptied, making the loading
410 # of children possible by accessing the `children` property.
411 #
412
413 unset($node->children);
414 }
415 else
416 {
417 self::setNodesDepth($node->children, $max_depth, $depth + 1);
418 }
419 }
420 }
421 }
422
423 /**
424 * Creates an array from all the nested nodes, where keys are node's Id.
425 *
426 * @param $nodes
427 */
428 static public function levelNodesById($nodes) // TODO-20120922: deprecate
429 {
430 $by_id = array();
431
432 foreach ($nodes as $node)
433 {
434 $by_id[$node->nid] = $node;
435
436 if (isset($node->children))
437 {
438 $by_id += self::levelNodesById($node->children);
439 }
440 }
441
442 return $by_id;
443 }
444 }