1<?php
2/**
3 * Joomla! Content Management System
4 *
5 * @copyright  Copyright (C) 2005 - 2020 Open Source Matters, Inc. All rights reserved.
6 * @license    GNU General Public License version 2 or later; see LICENSE.txt
7 */
8
9namespace Joomla\CMS\Helper;
10
11defined('JPATH_PLATFORM') or die;
12
13use Joomla\CMS\Component\ComponentHelper;
14use Joomla\CMS\Table\Table;
15use Joomla\CMS\Table\TableInterface;
16use Joomla\Utilities\ArrayHelper;
17
18/**
19 * Tags helper class, provides methods to perform various tasks relevant
20 * tagging of content.
21 *
22 * @since  3.1
23 */
24class TagsHelper extends CMSHelper
25{
26	/**
27	 * Helper object for storing and deleting tag information.
28	 *
29	 * @var    boolean
30	 * @since  3.1
31	 */
32	protected $tagsChanged = false;
33
34	/**
35	 * Whether up replace all tags or just add tags
36	 *
37	 * @var    boolean
38	 * @since  3.1
39	 */
40	protected $replaceTags = false;
41
42	/**
43	 * Alias for querying mapping and content type table.
44	 *
45	 * @var    string
46	 * @since  3.1
47	 */
48	public $typeAlias = null;
49
50	/**
51	 * Method to add tag rows to mapping table.
52	 *
53	 * @param   integer         $ucmId  ID of the #__ucm_content item being tagged
54	 * @param   TableInterface  $table  Table object being tagged
55	 * @param   array           $tags   Array of tags to be applied.
56	 *
57	 * @return  boolean  true on success, otherwise false.
58	 *
59	 * @since   3.1
60	 */
61	public function addTagMapping($ucmId, TableInterface $table, $tags = array())
62	{
63		$db = $table->getDbo();
64		$key = $table->getKeyName();
65		$item = $table->$key;
66		$typeId = $this->getTypeId($this->typeAlias);
67
68		// Insert the new tag maps
69		if (strpos('#', implode(',', $tags)) === false)
70		{
71			$tags = self::createTagsFromField($tags);
72		}
73
74		// Prevent saving duplicate tags
75		$tags = array_unique($tags);
76
77		$query = $db->getQuery(true);
78		$query->insert('#__contentitem_tag_map');
79		$query->columns(
80			array(
81				$db->quoteName('type_alias'),
82				$db->quoteName('core_content_id'),
83				$db->quoteName('content_item_id'),
84				$db->quoteName('tag_id'),
85				$db->quoteName('tag_date'),
86				$db->quoteName('type_id'),
87			)
88		);
89
90		foreach ($tags as $tag)
91		{
92			$query->values(
93				$db->quote($this->typeAlias)
94				. ', ' . (int) $ucmId
95				. ', ' . (int) $item
96				. ', ' . $db->quote($tag)
97				. ', ' . $query->currentTimestamp()
98				. ', ' . (int) $typeId
99			);
100		}
101
102		$db->setQuery($query);
103
104		return (boolean) $db->execute();
105	}
106
107	/**
108	 * Function that converts tags paths into paths of names
109	 *
110	 * @param   array  $tags  Array of tags
111	 *
112	 * @return  array
113	 *
114	 * @since   3.1
115	 */
116	public static function convertPathsToNames($tags)
117	{
118		// We will replace path aliases with tag names
119		if ($tags)
120		{
121			// Create an array with all the aliases of the results
122			$aliases = array();
123
124			foreach ($tags as $tag)
125			{
126				if (!empty($tag->path))
127				{
128					if ($pathParts = explode('/', $tag->path))
129					{
130						$aliases = array_merge($aliases, $pathParts);
131					}
132				}
133			}
134
135			// Get the aliases titles in one single query and map the results
136			if ($aliases)
137			{
138				// Remove duplicates
139				$aliases = array_unique($aliases);
140
141				$db = \JFactory::getDbo();
142
143				$query = $db->getQuery(true)
144					->select('alias, title')
145					->from('#__tags')
146					->where('alias IN (' . implode(',', array_map(array($db, 'quote'), $aliases)) . ')');
147				$db->setQuery($query);
148
149				try
150				{
151					$aliasesMapper = $db->loadAssocList('alias');
152				}
153				catch (\RuntimeException $e)
154				{
155					return false;
156				}
157
158				// Rebuild the items path
159				if ($aliasesMapper)
160				{
161					foreach ($tags as $tag)
162					{
163						$namesPath = array();
164
165						if (!empty($tag->path))
166						{
167							if ($pathParts = explode('/', $tag->path))
168							{
169								foreach ($pathParts as $alias)
170								{
171									if (isset($aliasesMapper[$alias]))
172									{
173										$namesPath[] = $aliasesMapper[$alias]['title'];
174									}
175									else
176									{
177										$namesPath[] = $alias;
178									}
179								}
180
181								$tag->text = implode('/', $namesPath);
182							}
183						}
184					}
185				}
186			}
187		}
188
189		return $tags;
190	}
191
192	/**
193	 * Create any new tags by looking for #new# in the strings
194	 *
195	 * @param   array  $tags  Tags text array from the field
196	 *
197	 * @return  mixed   If successful, metadata with new tag titles replaced by tag ids. Otherwise false.
198	 *
199	 * @since   3.1
200	 */
201	public function createTagsFromField($tags)
202	{
203		if (empty($tags) || $tags[0] == '')
204		{
205			return;
206		}
207		else
208		{
209			// We will use the tags table to store them
210			Table::addIncludePath(JPATH_ADMINISTRATOR . '/components/com_tags/tables');
211			$tagTable  = Table::getInstance('Tag', 'TagsTable');
212			$newTags   = array();
213			$canCreate = \JFactory::getUser()->authorise('core.create', 'com_tags');
214
215			foreach ($tags as $key => $tag)
216			{
217				// User is not allowed to create tags, so don't create.
218				if (!$canCreate && strpos($tag, '#new#') !== false)
219				{
220					continue;
221				}
222
223				// Remove the #new# prefix that identifies new tags
224				$tagText = str_replace('#new#', '', $tag);
225
226				if ($tagText === $tag)
227				{
228					$newTags[] = (int) $tag;
229				}
230				else
231				{
232					// Clear old data if exist
233					$tagTable->reset();
234
235					// Try to load the selected tag
236					if ($tagTable->load(array('title' => $tagText)))
237					{
238						$newTags[] = (int) $tagTable->id;
239					}
240					else
241					{
242						// Prepare tag data
243						$tagTable->id = 0;
244						$tagTable->title = $tagText;
245						$tagTable->published = 1;
246
247						// $tagTable->language = property_exists ($item, 'language') ? $item->language : '*';
248						$tagTable->language = '*';
249						$tagTable->access = 1;
250
251						// Make this item a child of the root tag
252						$tagTable->setLocation($tagTable->getRootId(), 'last-child');
253
254						// Try to store tag
255						if ($tagTable->check())
256						{
257							// Assign the alias as path (autogenerated tags have always level 1)
258							$tagTable->path = $tagTable->alias;
259
260							if ($tagTable->store())
261							{
262								$newTags[] = (int) $tagTable->id;
263							}
264						}
265					}
266				}
267			}
268
269			// At this point $tags is an array of all tag ids
270			$this->tags = $newTags;
271			$result = $newTags;
272		}
273
274		return $result;
275	}
276
277	/**
278	 * Create any new tags by looking for #new# in the metadata
279	 *
280	 * @param   string  $metadata  Metadata JSON string
281	 *
282	 * @return  mixed   If successful, metadata with new tag titles replaced by tag ids. Otherwise false.
283	 *
284	 * @since   3.1
285	 * @deprecated  4.0  This method is no longer used in the CMS and will not be replaced.
286	 */
287	public function createTagsFromMetadata($metadata)
288	{
289		$metaObject = json_decode($metadata);
290
291		if (empty($metaObject->tags))
292		{
293			return $metadata;
294		}
295
296		$tags = $metaObject->tags;
297
298		if (empty($tags) || !is_array($tags))
299		{
300			$result = $metadata;
301		}
302		else
303		{
304			// We will use the tags table to store them
305			Table::addIncludePath(JPATH_ADMINISTRATOR . '/components/com_tags/tables');
306			$tagTable = Table::getInstance('Tag', 'TagsTable');
307			$newTags = array();
308
309			foreach ($tags as $tag)
310			{
311				// Remove the #new# prefix that identifies new tags
312				$tagText = str_replace('#new#', '', $tag);
313
314				if ($tagText === $tag)
315				{
316					$newTags[] = (int) $tag;
317				}
318				else
319				{
320					// Clear old data if exist
321					$tagTable->reset();
322
323					// Try to load the selected tag
324					if ($tagTable->load(array('title' => $tagText)))
325					{
326						$newTags[] = (int) $tagTable->id;
327					}
328					else
329					{
330						// Prepare tag data
331						$tagTable->id = 0;
332						$tagTable->title = $tagText;
333						$tagTable->published = 1;
334
335						// $tagTable->language = property_exists ($item, 'language') ? $item->language : '*';
336						$tagTable->language = '*';
337						$tagTable->access = 1;
338
339						// Make this item a child of the root tag
340						$tagTable->setLocation($tagTable->getRootId(), 'last-child');
341
342						// Try to store tag
343						if ($tagTable->check())
344						{
345							// Assign the alias as path (autogenerated tags have always level 1)
346							$tagTable->path = $tagTable->alias;
347
348							if ($tagTable->store())
349							{
350								$newTags[] = (int) $tagTable->id;
351							}
352						}
353					}
354				}
355			}
356
357			// At this point $tags is an array of all tag ids
358			$metaObject->tags = $newTags;
359			$result = json_encode($metaObject);
360		}
361
362		return $result;
363	}
364
365	/**
366	 * Method to delete the tag mappings and #__ucm_content record for for an item
367	 *
368	 * @param   TableInterface  $table          Table object of content table where delete occurred
369	 * @param   integer|array   $contentItemId  ID of the content item. Or an array of key/value pairs with array key
370	 *                                          being a primary key name and value being the content item ID. Note
371	 *                                          multiple primary keys are not supported
372	 *
373	 * @return  boolean  true on success, false on failure
374	 *
375	 * @since   3.1
376	 * @throws  \InvalidArgumentException
377	 */
378	public function deleteTagData(TableInterface $table, $contentItemId)
379	{
380		$key = $table->getKeyName();
381
382		if (!is_array($contentItemId))
383		{
384			$contentItemId = array($key => $contentItemId);
385		}
386
387		// If we have multiple items for the content item primary key we currently don't support this so
388		// throw an InvalidArgumentException for now
389		if (count($contentItemId) != 1)
390		{
391			throw new \InvalidArgumentException('Multiple primary keys are not supported as a content item id');
392		}
393
394		$result = $this->unTagItem($contentItemId[$key], $table);
395
396		/** @var  \JTableCorecontent $ucmContentTable */
397		$ucmContentTable = Table::getInstance('Corecontent');
398
399		return $result && $ucmContentTable->deleteByContentId($contentItemId[$key], $this->typeAlias);
400	}
401
402	/**
403	 * Method to get a list of tags for an item, optionally with the tag data.
404	 *
405	 * @param   string   $contentType  Content type alias. Dot separated.
406	 * @param   integer  $id           Id of the item to retrieve tags for.
407	 * @param   boolean  $getTagData   If true, data from the tags table will be included, defaults to true.
408	 *
409	 * @return  array    Array of of tag objects
410	 *
411	 * @since   3.1
412	 */
413	public function getItemTags($contentType, $id, $getTagData = true)
414	{
415		// Initialize some variables.
416		$db = \JFactory::getDbo();
417		$query = $db->getQuery(true)
418			->select($db->quoteName('m.tag_id'))
419			->from($db->quoteName('#__contentitem_tag_map') . ' AS m ')
420			->where(
421				array(
422					$db->quoteName('m.type_alias') . ' = ' . $db->quote($contentType),
423					$db->quoteName('m.content_item_id') . ' = ' . (int) $id,
424					$db->quoteName('t.published') . ' = 1',
425				)
426			);
427
428		$user = \JFactory::getUser();
429		$groups = implode(',', $user->getAuthorisedViewLevels());
430
431		$query->where('t.access IN (' . $groups . ')');
432
433		// Optionally filter on language
434		$language = ComponentHelper::getParams('com_tags')->get('tag_list_language_filter', 'all');
435
436		if ($language !== 'all')
437		{
438			if ($language === 'current_language')
439			{
440				$language = $this->getCurrentLanguage();
441			}
442
443			$query->where($db->quoteName('language') . ' IN (' . $db->quote($language) . ', ' . $db->quote('*') . ')');
444		}
445
446		if ($getTagData)
447		{
448			$query->select($db->quoteName('t') . '.*');
449		}
450
451		$query->join('INNER', $db->quoteName('#__tags') . ' AS t ' . ' ON ' . $db->quoteName('m.tag_id') . ' = ' . $db->quoteName('t.id'));
452
453		$db->setQuery($query);
454		$this->itemTags = $db->loadObjectList();
455
456		return $this->itemTags;
457	}
458
459	/**
460	 * Method to get a list of tags for a given item.
461	 * Normally used for displaying a list of tags within a layout
462	 *
463	 * @param   mixed   $ids     The id or array of ids (primary key) of the item to be tagged.
464	 * @param   string  $prefix  Dot separated string with the option and view to be used for a url.
465	 *
466	 * @return  string   Comma separated list of tag Ids.
467	 *
468	 * @since   3.1
469	 */
470	public function getTagIds($ids, $prefix)
471	{
472		if (empty($ids))
473		{
474			return;
475		}
476
477		/**
478		 * Ids possible formats:
479		 * ---------------------
480		 * 	$id = 1;
481		 *  $id = array(1,2);
482		 *  $id = array('1,3,4,19');
483		 *  $id = '1,3';
484		 */
485		$ids = (array) $ids;
486		$ids = implode(',', $ids);
487		$ids = explode(',', $ids);
488		$ids = ArrayHelper::toInteger($ids);
489
490		$db = \JFactory::getDbo();
491
492		// Load the tags.
493		$query = $db->getQuery(true)
494			->select($db->quoteName('t.id'))
495			->from($db->quoteName('#__tags') . ' AS t ')
496			->join(
497				'INNER', $db->quoteName('#__contentitem_tag_map') . ' AS m'
498				. ' ON ' . $db->quoteName('m.tag_id') . ' = ' . $db->quoteName('t.id')
499				. ' AND ' . $db->quoteName('m.type_alias') . ' = ' . $db->quote($prefix)
500				. ' AND ' . $db->quoteName('m.content_item_id') . ' IN ( ' . implode(',', $ids) . ')'
501			);
502
503		$db->setQuery($query);
504
505		// Add the tags to the content data.
506		$tagsList = $db->loadColumn();
507		$this->tags = implode(',', $tagsList);
508
509		return $this->tags;
510	}
511
512	/**
513	 * Method to get a query to retrieve a detailed list of items for a tag.
514	 *
515	 * @param   mixed    $tagId            Tag or array of tags to be matched
516	 * @param   mixed    $typesr           Null, type or array of type aliases for content types to be included in the results
517	 * @param   boolean  $includeChildren  True to include the results from child tags
518	 * @param   string   $orderByOption    Column to order the results by
519	 * @param   string   $orderDir         Direction to sort the results in
520	 * @param   boolean  $anyOrAll         True to include items matching at least one tag, false to include
521	 *                                     items all tags in the array.
522	 * @param   string   $languageFilter   Optional filter on language. Options are 'all', 'current' or any string.
523	 * @param   string   $stateFilter      Optional filtering on publication state, defaults to published or unpublished.
524	 *
525	 * @return  \JDatabaseQuery  Query to retrieve a list of tags
526	 *
527	 * @since   3.1
528	 */
529	public function getTagItemsQuery($tagId, $typesr = null, $includeChildren = false, $orderByOption = 'c.core_title', $orderDir = 'ASC',
530		$anyOrAll = true, $languageFilter = 'all', $stateFilter = '0,1')
531	{
532		// Create a new query object.
533		$db = \JFactory::getDbo();
534		$query = $db->getQuery(true);
535		$user = \JFactory::getUser();
536		$nullDate = $db->quote($db->getNullDate());
537		$nowDate = $db->quote(\JFactory::getDate()->toSql());
538
539		// Force ids to array and sanitize
540		$tagIds = (array) $tagId;
541		$tagIds = implode(',', $tagIds);
542		$tagIds = explode(',', $tagIds);
543		$tagIds = ArrayHelper::toInteger($tagIds);
544
545		$ntagsr = count($tagIds);
546
547		// If we want to include children we have to adjust the list of tags.
548		// We do not search child tags when the match all option is selected.
549		if ($includeChildren)
550		{
551			$tagTreeArray = array();
552
553			foreach ($tagIds as $tag)
554			{
555				$this->getTagTreeArray($tag, $tagTreeArray);
556			}
557
558			$tagIds = array_unique(array_merge($tagIds, $tagTreeArray));
559		}
560
561		// Sanitize filter states
562		$stateFilters = explode(',', $stateFilter);
563		$stateFilters = ArrayHelper::toInteger($stateFilters);
564
565		// M is the mapping table. C is the core_content table. Ct is the content_types table.
566		$query
567			->select(
568				'm.type_alias'
569				. ', ' . 'm.content_item_id'
570				. ', ' . 'm.core_content_id'
571				. ', ' . 'count(m.tag_id) AS match_count'
572				. ', ' . 'MAX(m.tag_date) as tag_date'
573				. ', ' . 'MAX(c.core_title) AS core_title'
574				. ', ' . 'MAX(c.core_params) AS core_params'
575			)
576			->select('MAX(c.core_alias) AS core_alias, MAX(c.core_body) AS core_body, MAX(c.core_state) AS core_state, MAX(c.core_access) AS core_access')
577			->select(
578				'MAX(c.core_metadata) AS core_metadata'
579				. ', ' . 'MAX(c.core_created_user_id) AS core_created_user_id'
580				. ', ' . 'MAX(c.core_created_by_alias) AS core_created_by_alias'
581			)
582			->select('MAX(c.core_created_time) as core_created_time, MAX(c.core_images) as core_images')
583			->select('CASE WHEN c.core_modified_time = ' . $nullDate . ' THEN c.core_created_time ELSE c.core_modified_time END as core_modified_time')
584			->select('MAX(c.core_language) AS core_language, MAX(c.core_catid) AS core_catid')
585			->select('MAX(c.core_publish_up) AS core_publish_up, MAX(c.core_publish_down) as core_publish_down')
586			->select('MAX(ct.type_title) AS content_type_title, MAX(ct.router) AS router')
587
588			->from('#__contentitem_tag_map AS m')
589			->join(
590				'INNER',
591				'#__ucm_content AS c ON m.type_alias = c.core_type_alias AND m.core_content_id = c.core_content_id AND c.core_state IN ('
592					. implode(',', $stateFilters) . ')'
593					. (in_array('0', $stateFilters) ? '' : ' AND (c.core_publish_up = ' . $nullDate
594					. ' OR c.core_publish_up <= ' . $nowDate . ') '
595					. ' AND (c.core_publish_down = ' . $nullDate . ' OR  c.core_publish_down >= ' . $nowDate . ')')
596			)
597			->join('INNER', '#__content_types AS ct ON ct.type_alias = m.type_alias')
598
599			// Join over categories for get only tags from published categories
600			->join('LEFT', '#__categories AS tc ON tc.id = c.core_catid')
601
602			// Join over the users for the author and email
603			->select("CASE WHEN c.core_created_by_alias > ' ' THEN c.core_created_by_alias ELSE ua.name END AS author")
604			->select('ua.email AS author_email')
605
606			->join('LEFT', '#__users AS ua ON ua.id = c.core_created_user_id')
607
608			->where('m.tag_id IN (' . implode(',', $tagIds) . ')')
609			->where('(c.core_catid = 0 OR tc.published = 1)');
610
611		// Optionally filter on language
612		if (empty($language))
613		{
614			$language = $languageFilter;
615		}
616
617		if ($language !== 'all')
618		{
619			if ($language === 'current_language')
620			{
621				$language = $this->getCurrentLanguage();
622			}
623
624			$query->where($db->quoteName('c.core_language') . ' IN (' . $db->quote($language) . ', ' . $db->quote('*') . ')');
625		}
626
627		// Get the type data, limited to types in the request if there are any specified.
628		$typesarray = self::getTypes('assocList', $typesr, false);
629
630		$typeAliases = array();
631
632		foreach ($typesarray as $type)
633		{
634			$typeAliases[] = $db->quote($type['type_alias']);
635		}
636
637		$query->where('m.type_alias IN (' . implode(',', $typeAliases) . ')');
638
639		$groups = '0,' . implode(',', array_unique($user->getAuthorisedViewLevels()));
640		$query->where('c.core_access IN (' . $groups . ')')
641			->group('m.type_alias, m.content_item_id, m.core_content_id, core_modified_time, core_created_time, core_created_by_alias, author, author_email');
642
643		// Use HAVING if matching all tags and we are matching more than one tag.
644		if ($ntagsr > 1 && $anyOrAll != 1 && $includeChildren != 1)
645		{
646			// The number of results should equal the number of tags requested.
647			$query->having("COUNT('m.tag_id') = " . (int) $ntagsr);
648		}
649
650		// Set up the order by using the option chosen
651		if ($orderByOption === 'match_count')
652		{
653			$orderBy = 'COUNT(m.tag_id)';
654		}
655		else
656		{
657			$orderBy = 'MAX(' . $db->quoteName($orderByOption) . ')';
658		}
659
660		$query->order($orderBy . ' ' . $orderDir);
661
662		return $query;
663	}
664
665	/**
666	 * Function that converts tag ids to their tag names
667	 *
668	 * @param   array  $tagIds  Array of integer tag ids.
669	 *
670	 * @return  array  An array of tag names.
671	 *
672	 * @since   3.1
673	 */
674	public function getTagNames($tagIds)
675	{
676		$tagNames = array();
677
678		if (is_array($tagIds) && count($tagIds) > 0)
679		{
680			$tagIds = ArrayHelper::toInteger($tagIds);
681
682			$db = \JFactory::getDbo();
683			$query = $db->getQuery(true)
684				->select($db->quoteName('title'))
685				->from($db->quoteName('#__tags'))
686				->where($db->quoteName('id') . ' IN (' . implode(',', $tagIds) . ')');
687			$query->order($db->quoteName('title'));
688
689			$db->setQuery($query);
690			$tagNames = $db->loadColumn();
691		}
692
693		return $tagNames;
694	}
695
696	/**
697	 * Method to get an array of tag ids for the current tag and its children
698	 *
699	 * @param   integer  $id             An optional ID
700	 * @param   array    &$tagTreeArray  Array containing the tag tree
701	 *
702	 * @return  mixed
703	 *
704	 * @since   3.1
705	 */
706	public function getTagTreeArray($id, &$tagTreeArray = array())
707	{
708		// Get a level row instance.
709		Table::addIncludePath(JPATH_ADMINISTRATOR . '/components/com_tags/tables');
710		$table = Table::getInstance('Tag', 'TagsTable');
711
712		if ($table->isLeaf($id))
713		{
714			$tagTreeArray[] = $id;
715
716			return $tagTreeArray;
717		}
718
719		$tagTree = $table->getTree($id);
720
721		// Attempt to load the tree
722		if ($tagTree)
723		{
724			foreach ($tagTree as $tag)
725			{
726				$tagTreeArray[] = $tag->id;
727			}
728
729			return $tagTreeArray;
730		}
731	}
732
733	/**
734	 * Method to get the type id for a type alias.
735	 *
736	 * @param   string  $typeAlias  A type alias.
737	 *
738	 * @return  string  Name of the table for a type
739	 *
740	 * @since   3.1
741	 * @deprecated  4.0  Use \JUcmType::getTypeId() instead
742	 */
743	public function getTypeId($typeAlias)
744	{
745		$contentType = new \JUcmType;
746
747		return $contentType->getTypeId($typeAlias);
748	}
749
750	/**
751	 * Method to get a list of types with associated data.
752	 *
753	 * @param   string   $arrayType    Optionally specify that the returned list consist of objects, associative arrays, or arrays.
754	 *                                 Options are: rowList, assocList, and objectList
755	 * @param   array    $selectTypes  Optional array of type ids to limit the results to. Often from a request.
756	 * @param   boolean  $useAlias     If true, the alias is used to match, if false the type_id is used.
757	 *
758	 * @return  array   Array of of types
759	 *
760	 * @since   3.1
761	 */
762	public static function getTypes($arrayType = 'objectList', $selectTypes = null, $useAlias = true)
763	{
764		// Initialize some variables.
765		$db = \JFactory::getDbo();
766		$query = $db->getQuery(true)
767			->select('*');
768
769		if (!empty($selectTypes))
770		{
771			$selectTypes = (array) $selectTypes;
772
773			if ($useAlias)
774			{
775				$selectTypes = array_map(array($db, 'quote'), $selectTypes);
776
777				$query->where($db->quoteName('type_alias') . ' IN (' . implode(',', $selectTypes) . ')');
778			}
779			else
780			{
781				$selectTypes = ArrayHelper::toInteger($selectTypes);
782
783				$query->where($db->quoteName('type_id') . ' IN (' . implode(',', $selectTypes) . ')');
784			}
785		}
786
787		$query->from($db->quoteName('#__content_types'));
788
789		$db->setQuery($query);
790
791		switch ($arrayType)
792		{
793			case 'assocList':
794				$types = $db->loadAssocList();
795				break;
796
797			case 'rowList':
798				$types = $db->loadRowList();
799				break;
800
801			case 'objectList':
802			default:
803				$types = $db->loadObjectList();
804				break;
805		}
806
807		return $types;
808	}
809
810	/**
811	 * Function that handles saving tags used in a table class after a store()
812	 *
813	 * @param   TableInterface  $table    Table being processed
814	 * @param   array           $newTags  Array of new tags
815	 * @param   boolean         $replace  Flag indicating if all existing tags should be replaced
816	 *
817	 * @return  boolean
818	 *
819	 * @since   3.1
820	 */
821	public function postStoreProcess(TableInterface $table, $newTags = array(), $replace = true)
822	{
823		if (!empty($table->newTags) && empty($newTags))
824		{
825			$newTags = $table->newTags;
826		}
827
828		// If existing row, check to see if tags have changed.
829		$newTable = clone $table;
830		$newTable->reset();
831
832		$result = true;
833
834		// Process ucm_content and ucm_base if either tags have changed or we have some tags.
835		if ($this->tagsChanged || (!empty($newTags) && $newTags[0] != ''))
836		{
837			if (!$newTags && $replace == true)
838			{
839				// Delete all tags data
840				$key = $table->getKeyName();
841				$result = $this->deleteTagData($table, $table->$key);
842			}
843			else
844			{
845				// Process the tags
846				$data = $this->getRowData($table);
847				$ucmContentTable = Table::getInstance('Corecontent');
848
849				$ucm = new \JUcmContent($table, $this->typeAlias);
850				$ucmData = $data ? $ucm->mapData($data) : $ucm->ucmData;
851
852				$primaryId = $ucm->getPrimaryKey($ucmData['common']['core_type_id'], $ucmData['common']['core_content_item_id']);
853				$result = $ucmContentTable->load($primaryId);
854				$result = $result && $ucmContentTable->bind($ucmData['common']);
855				$result = $result && $ucmContentTable->check();
856				$result = $result && $ucmContentTable->store();
857				$ucmId = $ucmContentTable->core_content_id;
858
859				// Store the tag data if the article data was saved and run related methods.
860				$result = $result && $this->tagItem($ucmId, $table, $newTags, $replace);
861			}
862		}
863
864		return $result;
865	}
866
867	/**
868	 * Function that preProcesses data from a table prior to a store() to ensure proper tag handling
869	 *
870	 * @param   TableInterface  $table    Table being processed
871	 * @param   array           $newTags  Array of new tags
872	 *
873	 * @return  null
874	 *
875	 * @since   3.1
876	 */
877	public function preStoreProcess(TableInterface $table, $newTags = array())
878	{
879		if ($newTags != array())
880		{
881			$this->newTags = $newTags;
882		}
883
884		// If existing row, check to see if tags have changed.
885		$oldTable = clone $table;
886		$oldTable->reset();
887		$key = $oldTable->getKeyName();
888		$typeAlias = $this->typeAlias;
889
890		if ($oldTable->$key && $oldTable->load())
891		{
892			$this->oldTags = $this->getTagIds($oldTable->$key, $typeAlias);
893		}
894
895		// New items with no tags bypass this step.
896		if ((!empty($newTags) && is_string($newTags) || (isset($newTags[0]) && $newTags[0] != '')) || isset($this->oldTags))
897		{
898			if (is_array($newTags))
899			{
900				$newTags = implode(',', $newTags);
901			}
902
903			// We need to process tags if the tags have changed or if we have a new row
904			$this->tagsChanged = (empty($this->oldTags) && !empty($newTags)) ||(!empty($this->oldTags) && $this->oldTags != $newTags) || !$table->$key;
905		}
906	}
907
908	/**
909	 * Function to search tags
910	 *
911	 * @param   array  $filters  Filter to apply to the search
912	 *
913	 * @return  array
914	 *
915	 * @since   3.1
916	 */
917	public static function searchTags($filters = array())
918	{
919		$db = \JFactory::getDbo();
920		$query = $db->getQuery(true)
921			->select('a.id AS value')
922			->select('a.path AS text')
923			->select('a.path')
924			->from('#__tags AS a')
925			->join('LEFT', $db->quoteName('#__tags', 'b') . ' ON a.lft > b.lft AND a.rgt < b.rgt');
926
927		// Filter language
928		if (!empty($filters['flanguage']))
929		{
930			$query->where('a.language IN (' . $db->quote($filters['flanguage']) . ',' . $db->quote('*') . ') ');
931		}
932
933		// Do not return root
934		$query->where($db->quoteName('a.alias') . ' <> ' . $db->quote('root'));
935
936		// Search in title or path
937		if (!empty($filters['like']))
938		{
939			$query->where(
940				'(' . $db->quoteName('a.title') . ' LIKE ' . $db->quote('%' . $filters['like'] . '%')
941					. ' OR ' . $db->quoteName('a.path') . ' LIKE ' . $db->quote('%' . $filters['like'] . '%') . ')'
942			);
943		}
944
945		// Filter title
946		if (!empty($filters['title']))
947		{
948			$query->where($db->quoteName('a.title') . ' = ' . $db->quote($filters['title']));
949		}
950
951		// Filter on the published state
952		if (isset($filters['published']) && is_numeric($filters['published']))
953		{
954			$query->where('a.published = ' . (int) $filters['published']);
955		}
956
957		// Filter on the access level
958		if (isset($filters['access']) && is_array($filters['access']) && count($filters['access']))
959		{
960			$groups = ArrayHelper::toInteger($filters['access']);
961			$query->where('a.access IN (' . implode(",", $groups) . ')');
962		}
963
964		// Filter by parent_id
965		if (isset($filters['parent_id']) && is_numeric($filters['parent_id']))
966		{
967			Table::addIncludePath(JPATH_ADMINISTRATOR . '/components/com_tags/tables');
968			$tagTable = Table::getInstance('Tag', 'TagsTable');
969
970			if ($children = $tagTable->getTree($filters['parent_id']))
971			{
972				foreach ($children as $child)
973				{
974					$childrenIds[] = $child->id;
975				}
976
977				$query->where('a.id IN (' . implode(',', $childrenIds) . ')');
978			}
979		}
980
981		$query->group('a.id, a.title, a.level, a.lft, a.rgt, a.parent_id, a.published, a.path')
982			->order('a.lft ASC');
983
984		// Get the options.
985		$db->setQuery($query);
986
987		try
988		{
989			$results = $db->loadObjectList();
990		}
991		catch (\RuntimeException $e)
992		{
993			return array();
994		}
995
996		// We will replace path aliases with tag names
997		return self::convertPathsToNames($results);
998	}
999
1000	/**
1001	 * Method to delete all instances of a tag from the mapping table. Generally used when a tag is deleted.
1002	 *
1003	 * @param   integer  $tagId  The tag_id (primary key) for the deleted tag.
1004	 *
1005	 * @return  void
1006	 *
1007	 * @since   3.1
1008	 */
1009	public function tagDeleteInstances($tagId)
1010	{
1011		// Delete the old tag maps.
1012		$db = \JFactory::getDbo();
1013		$query = $db->getQuery(true)
1014			->delete($db->quoteName('#__contentitem_tag_map'))
1015			->where($db->quoteName('tag_id') . ' = ' . (int) $tagId);
1016		$db->setQuery($query);
1017		$db->execute();
1018	}
1019
1020	/**
1021	 * Method to add or update tags associated with an item.
1022	 *
1023	 * @param   integer         $ucmId    Id of the #__ucm_content item being tagged
1024	 * @param   TableInterface  $table    Table object being tagged
1025	 * @param   array           $tags     Array of tags to be applied.
1026	 * @param   boolean         $replace  Flag indicating if all existing tags should be replaced
1027	 *
1028	 * @return  boolean  true on success, otherwise false.
1029	 *
1030	 * @since   3.1
1031	 */
1032	public function tagItem($ucmId, TableInterface $table, $tags = array(), $replace = true)
1033	{
1034		$key = $table->get('_tbl_key');
1035		$oldTags = $this->getTagIds((int) $table->$key, $this->typeAlias);
1036		$oldTags = explode(',', $oldTags);
1037		$result = $this->unTagItem($ucmId, $table);
1038
1039		if ($replace)
1040		{
1041			$newTags = $tags;
1042		}
1043		else
1044		{
1045			if ($tags == array())
1046			{
1047				$newTags = $table->newTags;
1048			}
1049			else
1050			{
1051				$newTags = $tags;
1052			}
1053
1054			if ($oldTags[0] != '')
1055			{
1056				$newTags = array_unique(array_merge($newTags, $oldTags));
1057			}
1058		}
1059
1060		if (is_array($newTags) && count($newTags) > 0 && $newTags[0] != '')
1061		{
1062			$result = $result && $this->addTagMapping($ucmId, $table, $newTags);
1063		}
1064
1065		return $result;
1066	}
1067
1068	/**
1069	 * Method to untag an item
1070	 *
1071	 * @param   integer         $contentId  ID of the content item being untagged
1072	 * @param   TableInterface  $table      Table object being untagged
1073	 * @param   array           $tags       Array of tags to be untagged. Use an empty array to untag all existing tags.
1074	 *
1075	 * @return  boolean  true on success, otherwise false.
1076	 *
1077	 * @since   3.1
1078	 */
1079	public function unTagItem($contentId, TableInterface $table, $tags = array())
1080	{
1081		$key = $table->getKeyName();
1082		$id = $table->$key;
1083		$db = \JFactory::getDbo();
1084		$query = $db->getQuery(true)
1085			->delete('#__contentitem_tag_map')
1086			->where($db->quoteName('type_alias') . ' = ' . $db->quote($this->typeAlias))
1087			->where($db->quoteName('content_item_id') . ' = ' . (int) $id);
1088
1089		if (is_array($tags) && count($tags) > 0)
1090		{
1091			$tags = ArrayHelper::toInteger($tags);
1092
1093			$query->where($db->quoteName('tag_id') . ' IN (' . implode(',', $tags) . ')');
1094		}
1095
1096		$db->setQuery($query);
1097
1098		return (boolean) $db->execute();
1099	}
1100}
1101