1 <?php
2 /**
3 * @package Joomla.Legacy
4 * @subpackage Categories
5 *
6 * @copyright Copyright (C) 2005 - 2017 Open Source Matters, Inc. All rights reserved.
7 * @license GNU General Public License version 2 or later; see LICENSE.txt
8 */
9
10 defined('JPATH_PLATFORM') or die;
11
12 use Joomla\Registry\Registry;
13
14 /**
15 * JCategories Class.
16 *
17 * @since 1.6
18 */
19 class JCategories
20 {
21 /**
22 * Array to hold the object instances
23 *
24 * @var JCategories[]
25 * @since 1.6
26 */
27 public static $instances = array();
28
29 /**
30 * Array of category nodes
31 *
32 * @var JCategoryNode[]
33 * @since 1.6
34 */
35 protected $_nodes;
36
37 /**
38 * Array of checked categories -- used to save values when _nodes are null
39 *
40 * @var boolean[]
41 * @since 1.6
42 */
43 protected $_checkedCategories;
44
45 /**
46 * Name of the extension the categories belong to
47 *
48 * @var string
49 * @since 1.6
50 */
51 protected $_extension = null;
52
53 /**
54 * Name of the linked content table to get category content count
55 *
56 * @var string
57 * @since 1.6
58 */
59 protected $_table = null;
60
61 /**
62 * Name of the category field
63 *
64 * @var string
65 * @since 1.6
66 */
67 protected $_field = null;
68
69 /**
70 * Name of the key field
71 *
72 * @var string
73 * @since 1.6
74 */
75 protected $_key = null;
76
77 /**
78 * Name of the items state field
79 *
80 * @var string
81 * @since 1.6
82 */
83 protected $_statefield = null;
84
85 /**
86 * Array of options
87 *
88 * @var array
89 * @since 1.6
90 */
91 protected $_options = null;
92
93 /**
94 * Class constructor
95 *
96 * @param array $options Array of options
97 *
98 * @since 1.6
99 */
100 public function __construct($options)
101 {
102 $this->_extension = $options['extension'];
103 $this->_table = $options['table'];
104 $this->_field = isset($options['field']) && $options['field'] ? $options['field'] : 'catid';
105 $this->_key = isset($options['key']) && $options['key'] ? $options['key'] : 'id';
106 $this->_statefield = isset($options['statefield']) ? $options['statefield'] : 'state';
107
108 $options['access'] = isset($options['access']) ? $options['access'] : 'true';
109 $options['published'] = isset($options['published']) ? $options['published'] : 1;
110 $options['countItems'] = isset($options['countItems']) ? $options['countItems'] : 0;
111 $options['currentlang'] = JLanguageMultilang::isEnabled() ? JFactory::getLanguage()->getTag() : 0;
112
113 $this->_options = $options;
114
115 return true;
116 }
117
118 /**
119 * Returns a reference to a JCategories object
120 *
121 * @param string $extension Name of the categories extension
122 * @param array $options An array of options
123 *
124 * @return JCategories|boolean JCategories object on success, boolean false if an object does not exist
125 *
126 * @since 1.6
127 */
128 public static function getInstance($extension, $options = array())
129 {
130 $hash = md5(strtolower($extension) . serialize($options));
131
132 if (isset(self::$instances[$hash]))
133 {
134 return self::$instances[$hash];
135 }
136
137 $parts = explode('.', $extension);
138 $component = 'com_' . strtolower($parts[0]);
139 $section = count($parts) > 1 ? $parts[1] : '';
140 $classname = ucfirst(substr($component, 4)) . ucfirst($section) . 'Categories';
141
142 if (!class_exists($classname))
143 {
144 $path = JPATH_SITE . '/components/' . $component . '/helpers/category.php';
145
146 JLoader::register($classname, $path);
147
148 if (!class_exists($classname))
149 {
150 return false;
151 }
152 }
153
154 self::$instances[$hash] = new $classname($options);
155
156 return self::$instances[$hash];
157 }
158
159 /**
160 * Loads a specific category and all its children in a JCategoryNode object
161 *
162 * @param mixed $id an optional id integer or equal to 'root'
163 * @param boolean $forceload True to force the _load method to execute
164 *
165 * @return JCategoryNode|null|boolean JCategoryNode object or null if $id is not valid
166 *
167 * @since 1.6
168 */
169 public function get($id = 'root', $forceload = false)
170 {
171 if ($id !== 'root')
172 {
173 $id = (int) $id;
174
175 if ($id == 0)
176 {
177 $id = 'root';
178 }
179 }
180
181 // If this $id has not been processed yet, execute the _load method
182 if ((!isset($this->_nodes[$id]) && !isset($this->_checkedCategories[$id])) || $forceload)
183 {
184 $this->_load($id);
185 }
186
187 // If we already have a value in _nodes for this $id, then use it.
188 if (isset($this->_nodes[$id]))
189 {
190 return $this->_nodes[$id];
191 }
192 // If we processed this $id already and it was not valid, then return null.
193 elseif (isset($this->_checkedCategories[$id]))
194 {
195 return;
196 }
197
198 return false;
199 }
200
201 /**
202 * Load method
203 *
204 * @param integer $id Id of category to load
205 *
206 * @return void
207 *
208 * @since 1.6
209 */
210 protected function _load($id)
211 {
212 $db = JFactory::getDbo();
213 $app = JFactory::getApplication();
214 $user = JFactory::getUser();
215 $extension = $this->_extension;
216
217 // Record that has this $id has been checked
218 $this->_checkedCategories[$id] = true;
219
220 $query = $db->getQuery(true);
221
222 // Right join with c for category
223 $query->select('c.id, c.asset_id, c.access, c.alias, c.checked_out, c.checked_out_time,
224 c.created_time, c.created_user_id, c.description, c.extension, c.hits, c.language, c.level,
225 c.lft, c.metadata, c.metadesc, c.metakey, c.modified_time, c.note, c.params, c.parent_id,
226 c.path, c.published, c.rgt, c.title, c.modified_user_id, c.version');
227 $case_when = ' CASE WHEN ';
228 $case_when .= $query->charLength('c.alias', '!=', '0');
229 $case_when .= ' THEN ';
230 $c_id = $query->castAsChar('c.id');
231 $case_when .= $query->concatenate(array($c_id, 'c.alias'), ':');
232 $case_when .= ' ELSE ';
233 $case_when .= $c_id . ' END as slug';
234 $query->select($case_when)
235 ->from('#__categories as c')
236 ->where('(c.extension=' . $db->quote($extension) . ' OR c.extension=' . $db->quote('system') . ')');
237
238 if ($this->_options['access'])
239 {
240 $query->where('c.access IN (' . implode(',', $user->getAuthorisedViewLevels()) . ')');
241 }
242
243 if ($this->_options['published'] == 1)
244 {
245 $query->where('c.published = 1');
246 }
247
248 $query->order('c.lft');
249
250 // Note: s for selected id
251 if ($id != 'root')
252 {
253 // Get the selected category
254 $query->where('s.id=' . (int) $id);
255
256 if ($app->isClient('site') && JLanguageMultilang::isEnabled())
257 {
258 $query->join('LEFT', '#__categories AS s ON (s.lft < c.lft AND s.rgt > c.rgt AND c.language in (' . $db->quote(JFactory::getLanguage()->getTag())
259 . ',' . $db->quote('*') . ')) OR (s.lft >= c.lft AND s.rgt <= c.rgt)');
260 }
261 else
262 {
263 $query->join('LEFT', '#__categories AS s ON (s.lft <= c.lft AND s.rgt >= c.rgt) OR (s.lft > c.lft AND s.rgt < c.rgt)');
264 }
265 }
266 else
267 {
268 if ($app->isClient('site') && JLanguageMultilang::isEnabled())
269 {
270 $query->where('c.language in (' . $db->quote(JFactory::getLanguage()->getTag()) . ',' . $db->quote('*') . ')');
271 }
272 }
273
274 // Note: i for item
275 if ($this->_options['countItems'] == 1)
276 {
277 $queryjoin = $db->quoteName($this->_table) . ' AS i ON i.' . $db->quoteName($this->_field) . ' = c.id';
278
279 if ($this->_options['published'] == 1)
280 {
281 $queryjoin .= ' AND i.' . $this->_statefield . ' = 1';
282 }
283
284 if ($this->_options['currentlang'] !== 0)
285 {
286 $queryjoin .= ' AND (i.language = ' . $db->quote('*') . ' OR i.language = ' . $db->quote($this->_options['currentlang']) . ')';
287 }
288
289 $query->join('LEFT', $queryjoin);
290 $query->select('COUNT(i.' . $db->quoteName($this->_key) . ') AS numitems');
291
292 // Group by
293 $query->group(
294 'c.id, c.asset_id, c.access, c.alias, c.checked_out, c.checked_out_time,
295 c.created_time, c.created_user_id, c.description, c.extension, c.hits, c.language, c.level,
296 c.lft, c.metadata, c.metadesc, c.metakey, c.modified_time, c.note, c.params, c.parent_id,
297 c.path, c.published, c.rgt, c.title, c.modified_user_id, c.version'
298 );
299 }
300
301
302 // Get the results
303 $db->setQuery($query);
304 $results = $db->loadObjectList('id');
305 $childrenLoaded = false;
306
307 if (count($results))
308 {
309 // Foreach categories
310 foreach ($results as $result)
311 {
312 // Deal with root category
313 if ($result->id == 1)
314 {
315 $result->id = 'root';
316 }
317
318 // Deal with parent_id
319 if ($result->parent_id == 1)
320 {
321 $result->parent_id = 'root';
322 }
323
324 // Create the node
325 if (!isset($this->_nodes[$result->id]))
326 {
327 // Create the JCategoryNode and add to _nodes
328 $this->_nodes[$result->id] = new JCategoryNode($result, $this);
329
330 // If this is not root and if the current node's parent is in the list or the current node parent is 0
331 if ($result->id != 'root' && (isset($this->_nodes[$result->parent_id]) || $result->parent_id == 1))
332 {
333 // Compute relationship between node and its parent - set the parent in the _nodes field
334 $this->_nodes[$result->id]->setParent($this->_nodes[$result->parent_id]);
335 }
336
337 // If the node's parent id is not in the _nodes list and the node is not root (doesn't have parent_id == 0),
338 // then remove the node from the list
339 if (!(isset($this->_nodes[$result->parent_id]) || $result->parent_id == 0))
340 {
341 unset($this->_nodes[$result->id]);
342 continue;
343 }
344
345 if ($result->id == $id || $childrenLoaded)
346 {
347 $this->_nodes[$result->id]->setAllLoaded();
348 $childrenLoaded = true;
349 }
350 }
351 elseif ($result->id == $id || $childrenLoaded)
352 {
353 // Create the JCategoryNode
354 $this->_nodes[$result->id] = new JCategoryNode($result, $this);
355
356 if ($result->id != 'root' && (isset($this->_nodes[$result->parent_id]) || $result->parent_id))
357 {
358 // Compute relationship between node and its parent
359 $this->_nodes[$result->id]->setParent($this->_nodes[$result->parent_id]);
360 }
361
362 // If the node's parent id is not in the _nodes list and the node is not root (doesn't have parent_id == 0),
363 // then remove the node from the list
364 if (!(isset($this->_nodes[$result->parent_id]) || $result->parent_id == 0))
365 {
366 unset($this->_nodes[$result->id]);
367 continue;
368 }
369
370 if ($result->id == $id || $childrenLoaded)
371 {
372 $this->_nodes[$result->id]->setAllLoaded();
373 $childrenLoaded = true;
374 }
375 }
376 }
377 }
378 else
379 {
380 $this->_nodes[$id] = null;
381 }
382 }
383 }
384
385 /**
386 * Helper class to load Categorytree
387 *
388 * @since 1.6
389 */
390 class JCategoryNode extends JObject
391 {
392 /**
393 * Primary key
394 *
395 * @var integer
396 * @since 1.6
397 */
398 public $id = null;
399
400 /**
401 * The id of the category in the asset table
402 *
403 * @var integer
404 * @since 1.6
405 */
406 public $asset_id = null;
407
408 /**
409 * The id of the parent of category in the asset table, 0 for category root
410 *
411 * @var integer
412 * @since 1.6
413 */
414 public $parent_id = null;
415
416 /**
417 * The lft value for this category in the category tree
418 *
419 * @var integer
420 * @since 1.6
421 */
422 public $lft = null;
423
424 /**
425 * The rgt value for this category in the category tree
426 *
427 * @var integer
428 * @since 1.6
429 */
430 public $rgt = null;
431
432 /**
433 * The depth of this category's position in the category tree
434 *
435 * @var integer
436 * @since 1.6
437 */
438 public $level = null;
439
440 /**
441 * The extension this category is associated with
442 *
443 * @var integer
444 * @since 1.6
445 */
446 public $extension = null;
447
448 /**
449 * The menu title for the category (a short name)
450 *
451 * @var string
452 * @since 1.6
453 */
454 public $title = null;
455
456 /**
457 * The the alias for the category
458 *
459 * @var string
460 * @since 1.6
461 */
462 public $alias = null;
463
464 /**
465 * Description of the category.
466 *
467 * @var string
468 * @since 1.6
469 */
470 public $description = null;
471
472 /**
473 * The publication status of the category
474 *
475 * @var boolean
476 * @since 1.6
477 */
478 public $published = null;
479
480 /**
481 * Whether the category is or is not checked out
482 *
483 * @var boolean
484 * @since 1.6
485 */
486 public $checked_out = 0;
487
488 /**
489 * The time at which the category was checked out
490 *
491 * @var string
492 * @since 1.6
493 */
494 public $checked_out_time = 0;
495
496 /**
497 * Access level for the category
498 *
499 * @var integer
500 * @since 1.6
501 */
502 public $access = null;
503
504 /**
505 * JSON string of parameters
506 *
507 * @var string
508 * @since 1.6
509 */
510 public $params = null;
511
512 /**
513 * Metadata description
514 *
515 * @var string
516 * @since 1.6
517 */
518 public $metadesc = null;
519
520 /**
521 * Key words for meta data
522 *
523 * @var string
524 * @since 1.6
525 */
526 public $metakey = null;
527
528 /**
529 * JSON string of other meta data
530 *
531 * @var string
532 * @since 1.6
533 */
534 public $metadata = null;
535
536 /**
537 * The ID of the user who created the category
538 *
539 * @var integer
540 * @since 1.6
541 */
542 public $created_user_id = null;
543
544 /**
545 * The time at which the category was created
546 *
547 * @var string
548 * @since 1.6
549 */
550 public $created_time = null;
551
552 /**
553 * The ID of the user who last modified the category
554 *
555 * @var integer
556 * @since 1.6
557 */
558 public $modified_user_id = null;
559
560 /**
561 * The time at which the category was modified
562 *
563 * @var string
564 * @since 1.6
565 */
566 public $modified_time = null;
567
568 /**
569 * Nmber of times the category has been viewed
570 *
571 * @var integer
572 * @since 1.6
573 */
574 public $hits = null;
575
576 /**
577 * The language for the category in xx-XX format
578 *
579 * @var string
580 * @since 1.6
581 */
582 public $language = null;
583
584 /**
585 * Number of items in this category or descendants of this category
586 *
587 * @var integer
588 * @since 1.6
589 */
590 public $numitems = null;
591
592 /**
593 * Number of children items
594 *
595 * @var integer
596 * @since 1.6
597 */
598 public $childrennumitems = null;
599
600 /**
601 * Slug fo the category (used in URL)
602 *
603 * @var string
604 * @since 1.6
605 */
606 public $slug = null;
607
608 /**
609 * Array of assets
610 *
611 * @var array
612 * @since 1.6
613 */
614 public $assets = null;
615
616 /**
617 * Parent Category object
618 *
619 * @var JCategoryNode
620 * @since 1.6
621 */
622 protected $_parent = null;
623
624 /**
625 * Array of Children
626 *
627 * @var JCategoryNode[]
628 * @since 1.6
629 */
630 protected $_children = array();
631
632 /**
633 * Path from root to this category
634 *
635 * @var array
636 * @since 1.6
637 */
638 protected $_path = array();
639
640 /**
641 * Category left of this one
642 *
643 * @var JCategoryNode
644 * @since 1.6
645 */
646 protected $_leftSibling = null;
647
648 /**
649 * Category right of this one
650 *
651 * @var JCategoryNode
652 * @since 1.6
653 */
654 protected $_rightSibling = null;
655
656 /**
657 * Flag if all children have been loaded
658 *
659 * @var boolean
660 * @since 1.6
661 */
662 protected $_allChildrenloaded = false;
663
664 /**
665 * Constructor of this tree
666 *
667 * @var JCategoryNode
668 * @since 1.6
669 */
670 protected $_constructor = null;
671
672 /**
673 * Class constructor
674 *
675 * @param array $category The category data.
676 * @param JCategoryNode $constructor The tree constructor.
677 *
678 * @since 1.6
679 */
680 public function __construct($category = null, $constructor = null)
681 {
682 if ($category)
683 {
684 $this->setProperties($category);
685
686 if ($constructor)
687 {
688 $this->_constructor = $constructor;
689 }
690
691 return true;
692 }
693
694 return false;
695 }
696
697 /**
698 * Set the parent of this category
699 *
700 * If the category already has a parent, the link is unset
701 *
702 * @param JCategoryNode|null $parent JCategoryNode for the parent to be set or null
703 *
704 * @return void
705 *
706 * @since 1.6
707 */
708 public function setParent($parent)
709 {
710 if ($parent instanceof JCategoryNode || is_null($parent))
711 {
712 if (!is_null($this->_parent))
713 {
714 $key = array_search($this, $this->_parent->_children);
715 unset($this->_parent->_children[$key]);
716 }
717
718 if (!is_null($parent))
719 {
720 $parent->_children[] = & $this;
721 }
722
723 $this->_parent = $parent;
724
725 if ($this->id != 'root')
726 {
727 if ($this->parent_id != 1)
728 {
729 $this->_path = $parent->getPath();
730 }
731
732 $this->_path[$this->id] = $this->id . ':' . $this->alias;
733 }
734
735 if (count($parent->_children) > 1)
736 {
737 end($parent->_children);
738 $this->_leftSibling = prev($parent->_children);
739 $this->_leftSibling->_rightsibling = & $this;
740 }
741 }
742 }
743
744 /**
745 * Add child to this node
746 *
747 * If the child already has a parent, the link is unset
748 *
749 * @param JCategoryNode $child The child to be added.
750 *
751 * @return void
752 *
753 * @since 1.6
754 */
755 public function addChild($child)
756 {
757 if ($child instanceof JCategoryNode)
758 {
759 $child->setParent($this);
760 }
761 }
762
763 /**
764 * Remove a specific child
765 *
766 * @param integer $id ID of a category
767 *
768 * @return void
769 *
770 * @since 1.6
771 */
772 public function removeChild($id)
773 {
774 $key = array_search($this, $this->_parent->_children);
775 unset($this->_parent->_children[$key]);
776 }
777
778 /**
779 * Get the children of this node
780 *
781 * @param boolean $recursive False by default
782 *
783 * @return JCategoryNode[] The children
784 *
785 * @since 1.6
786 */
787 public function &getChildren($recursive = false)
788 {
789 if (!$this->_allChildrenloaded)
790 {
791 $temp = $this->_constructor->get($this->id, true);
792
793 if ($temp)
794 {
795 $this->_children = $temp->getChildren();
796 $this->_leftSibling = $temp->getSibling(false);
797 $this->_rightSibling = $temp->getSibling(true);
798 $this->setAllLoaded();
799 }
800 }
801
802 if ($recursive)
803 {
804 $items = array();
805
806 foreach ($this->_children as $child)
807 {
808 $items[] = $child;
809 $items = array_merge($items, $child->getChildren(true));
810 }
811
812 return $items;
813 }
814
815 return $this->_children;
816 }
817
818 /**
819 * Get the parent of this node
820 *
821 * @return JCategoryNode
822 *
823 * @since 1.6
824 */
825 public function getParent()
826 {
827 return $this->_parent;
828 }
829
830 /**
831 * Test if this node has children
832 *
833 * @return boolean True if there is a child
834 *
835 * @since 1.6
836 */
837 public function hasChildren()
838 {
839 return count($this->_children);
840 }
841
842 /**
843 * Test if this node has a parent
844 *
845 * @return boolean True if there is a parent
846 *
847 * @since 1.6
848 */
849 public function hasParent()
850 {
851 return $this->getParent() != null;
852 }
853
854 /**
855 * Function to set the left or right sibling of a category
856 *
857 * @param JCategoryNode $sibling JCategoryNode object for the sibling
858 * @param boolean $right If set to false, the sibling is the left one
859 *
860 * @return void
861 *
862 * @since 1.6
863 */
864 public function setSibling($sibling, $right = true)
865 {
866 if ($right)
867 {
868 $this->_rightSibling = $sibling;
869 }
870 else
871 {
872 $this->_leftSibling = $sibling;
873 }
874 }
875
876 /**
877 * Returns the right or left sibling of a category
878 *
879 * @param boolean $right If set to false, returns the left sibling
880 *
881 * @return JCategoryNode|null JCategoryNode object with the sibling information or null if there is no sibling on that side.
882 *
883 * @since 1.6
884 */
885 public function getSibling($right = true)
886 {
887 if (!$this->_allChildrenloaded)
888 {
889 $temp = $this->_constructor->get($this->id, true);
890 $this->_children = $temp->getChildren();
891 $this->_leftSibling = $temp->getSibling(false);
892 $this->_rightSibling = $temp->getSibling(true);
893 $this->setAllLoaded();
894 }
895
896 if ($right)
897 {
898 return $this->_rightSibling;
899 }
900 else
901 {
902 return $this->_leftSibling;
903 }
904 }
905
906 /**
907 * Returns the category parameters
908 *
909 * @return Registry
910 *
911 * @since 1.6
912 */
913 public function getParams()
914 {
915 if (!($this->params instanceof Registry))
916 {
917 $this->params = new Registry($this->params);
918 }
919
920 return $this->params;
921 }
922
923 /**
924 * Returns the category metadata
925 *
926 * @return Registry A Registry object containing the metadata
927 *
928 * @since 1.6
929 */
930 public function getMetadata()
931 {
932 if (!($this->metadata instanceof Registry))
933 {
934 $this->metadata = new Registry($this->metadata);
935 }
936
937 return $this->metadata;
938 }
939
940 /**
941 * Returns the category path to the root category
942 *
943 * @return array
944 *
945 * @since 1.6
946 */
947 public function getPath()
948 {
949 return $this->_path;
950 }
951
952 /**
953 * Returns the user that created the category
954 *
955 * @param boolean $modified_user Returns the modified_user when set to true
956 *
957 * @return JUser A JUser object containing a userid
958 *
959 * @since 1.6
960 */
961 public function getAuthor($modified_user = false)
962 {
963 if ($modified_user)
964 {
965 return JFactory::getUser($this->modified_user_id);
966 }
967
968 return JFactory::getUser($this->created_user_id);
969 }
970
971 /**
972 * Set to load all children
973 *
974 * @return void
975 *
976 * @since 1.6
977 */
978 public function setAllLoaded()
979 {
980 $this->_allChildrenloaded = true;
981
982 foreach ($this->_children as $child)
983 {
984 $child->setAllLoaded();
985 }
986 }
987
988 /**
989 * Returns the number of items.
990 *
991 * @param boolean $recursive If false number of children, if true number of descendants
992 *
993 * @return integer Number of children or descendants
994 *
995 * @since 1.6
996 */
997 public function getNumItems($recursive = false)
998 {
999 if ($recursive)
1000 {
1001 $count = $this->numitems;
1002
1003 foreach ($this->getChildren() as $child)
1004 {
1005 $count = $count + $child->getNumItems(true);
1006 }
1007
1008 return $count;
1009 }
1010
1011 return $this->numitems;
1012 }
1013 }
1014