Current File : /home/pacjaorg/www/km/components/com_finder/src/Model/SearchModel.php |
<?php
/**
* @package Joomla.Site
* @subpackage com_finder
*
* @copyright (C) 2011 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Component\Finder\Site\Model;
use Joomla\CMS\Factory;
use Joomla\CMS\Language\Multilanguage;
use Joomla\CMS\Language\Text;
use Joomla\CMS\MVC\Model\ListModel;
use Joomla\CMS\Plugin\PluginHelper;
use Joomla\CMS\Uri\Uri;
use Joomla\Component\Finder\Administrator\Indexer\Query;
use Joomla\String\StringHelper;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Search model class for the Finder package.
*
* @since 2.5
*/
class SearchModel extends ListModel
{
/**
* Context string for the model type
*
* @var string
* @since 2.5
*/
protected $context = 'com_finder.search';
/**
* The query object is an instance of Query which contains and
* models the entire search query including the text input; static and
* dynamic taxonomy filters; date filters; etc.
*
* @var Query
* @since 2.5
*/
protected $searchquery;
/**
* Maps each sorting field with a text label.
*
* @var string[]
*
* @since 4.3.0
*/
protected $sortOrderFieldsLabels = [
'relevance.asc' => 'COM_FINDER_SORT_BY_RELEVANCE_ASC',
'relevance.desc' => 'COM_FINDER_SORT_BY_RELEVANCE_DESC',
'title.asc' => 'JGLOBAL_TITLE_ASC',
'title.desc' => 'JGLOBAL_TITLE_DESC',
'date.asc' => 'JDATE_ASC',
'date.desc' => 'JDATE_DESC',
'price.asc' => 'COM_FINDER_SORT_BY_PRICE_ASC',
'price.desc' => 'COM_FINDER_SORT_BY_PRICE_DESC',
'sale_price.asc' => 'COM_FINDER_SORT_BY_SALES_PRICE_ASC',
'sale_price.desc' => 'COM_FINDER_SORT_BY_SALES_PRICE_DESC',
];
/**
* An array of all excluded terms ids.
*
* @var array
* @since 2.5
*/
protected $excludedTerms = [];
/**
* An array of all included terms ids.
*
* @var array
* @since 2.5
*/
protected $includedTerms = [];
/**
* An array of all required terms ids.
*
* @var array
* @since 2.5
*/
protected $requiredTerms = [];
/**
* Method to get the results of the query.
*
* @return array An array of Result objects.
*
* @since 2.5
* @throws \Exception on database error.
*/
public function getItems()
{
$items = parent::getItems();
// Check the data.
if (empty($items)) {
return null;
}
$results = [];
// Convert the rows to result objects.
foreach ($items as $rk => $row) {
// Build the result object.
if (is_resource($row->object)) {
$result = unserialize(stream_get_contents($row->object));
} else {
$result = unserialize($row->object);
}
$result->cleanURL = $result->route;
// Add the result back to the stack.
$results[] = $result;
}
// Return the results.
return $results;
}
/**
* Method to get the query object.
*
* @return Query A query object.
*
* @since 2.5
*/
public function getQuery()
{
// Return the query object.
return $this->searchquery;
}
/**
* Method to build a database query to load the list data.
*
* @return \Joomla\Database\DatabaseQuery A database query.
*
* @since 2.5
*/
protected function getListQuery()
{
// Create a new query object.
$db = $this->getDatabase();
$query = $db->getQuery(true);
// Select the required fields from the table.
$query->select(
$this->getState(
'list.select',
'l.link_id, l.object'
)
);
$query->from('#__finder_links AS l');
$user = $this->getCurrentUser();
$groups = $this->getState('user.groups', $user->getAuthorisedViewLevels());
$query->whereIn($db->quoteName('l.access'), $groups)
->where('l.state = 1')
->where('l.published = 1');
// Get the current date, minus seconds.
$nowDate = $db->quote(substr_replace(Factory::getDate()->toSql(), '00', -2));
// Add the publish up and publish down filters.
$query->where('(l.publish_start_date IS NULL OR l.publish_start_date <= ' . $nowDate . ')')
->where('(l.publish_end_date IS NULL OR l.publish_end_date >= ' . $nowDate . ')');
$query->group('l.link_id');
$query->group('l.object');
/*
* Add the taxonomy filters to the query. We have to join the taxonomy
* map table for each group so that we can use AND clauses across
* groups. Within each group there can be an array of values that will
* use OR clauses.
*/
if (!empty($this->searchquery->filters)) {
// Convert the associative array to a numerically indexed array.
$groups = array_values($this->searchquery->filters);
$taxonomies = call_user_func_array('array_merge', array_values($this->searchquery->filters));
$query->join('INNER', $db->quoteName('#__finder_taxonomy_map') . ' AS t ON t.link_id = l.link_id')
->where('t.node_id IN (' . implode(',', array_unique($taxonomies)) . ')');
// Iterate through each taxonomy group.
for ($i = 0, $c = count($groups); $i < $c; $i++) {
$query->having('SUM(CASE WHEN t.node_id IN (' . implode(',', $groups[$i]) . ') THEN 1 ELSE 0 END) > 0');
}
}
// Add the start date filter to the query.
if (!empty($this->searchquery->date1)) {
// Escape the date.
$date1 = $db->quote($this->searchquery->date1);
// Add the appropriate WHERE condition.
if ($this->searchquery->when1 === 'before') {
$query->where($db->quoteName('l.start_date') . ' <= ' . $date1);
} elseif ($this->searchquery->when1 === 'after') {
$query->where($db->quoteName('l.start_date') . ' >= ' . $date1);
} else {
$query->where($db->quoteName('l.start_date') . ' = ' . $date1);
}
}
// Add the end date filter to the query.
if (!empty($this->searchquery->date2)) {
// Escape the date.
$date2 = $db->quote($this->searchquery->date2);
// Add the appropriate WHERE condition.
if ($this->searchquery->when2 === 'before') {
$query->where($db->quoteName('l.start_date') . ' <= ' . $date2);
} elseif ($this->searchquery->when2 === 'after') {
$query->where($db->quoteName('l.start_date') . ' >= ' . $date2);
} else {
$query->where($db->quoteName('l.start_date') . ' = ' . $date2);
}
}
// Filter by language
if ($this->getState('filter.language')) {
$query->where('l.language IN (' . $db->quote(Factory::getLanguage()->getTag()) . ', ' . $db->quote('*') . ')');
}
// Get the result ordering and direction.
$ordering = $this->getState('list.ordering', 'm.weight');
$direction = $this->getState('list.direction', 'DESC');
/*
* If we are ordering by relevance we have to add up the relevance
* scores that are contained in the ordering field.
*/
if ($ordering === 'm.weight') {
// Get the base query and add the ordering information.
$query->select('SUM(' . $db->escape($ordering) . ') AS ordering');
} else {
/**
* If we are not ordering by relevance, we just have to add
* the unique items to the set.
*/
// Get the base query and add the ordering information.
$query->select($db->escape($ordering) . ' AS ordering');
}
$query->order('ordering ' . $db->escape($direction));
/*
* If there are no optional or required search terms in the query, we
* can get the results in one relatively simple database query.
*/
if (empty($this->includedTerms) && $this->searchquery->empty && $this->searchquery->input == '') {
// Return the results.
return $query;
}
/*
* If there are no optional or required search terms in the query and
* empty searches are not allowed, we return an empty query.
* If the search term is not empty and empty searches are allowed,
* but no terms were found, we return an empty query as well.
*/
if (
empty($this->includedTerms)
&& (!$this->searchquery->empty || ($this->searchquery->empty && $this->searchquery->input != ''))
) {
// Since we need to return a query, we simplify this one.
$query->clear('join')
->clear('where')
->clear('bounded')
->clear('having')
->clear('group')
->where('false');
return $query;
}
$included = call_user_func_array('array_merge', array_values($this->includedTerms));
$query->join('INNER', $db->quoteName('#__finder_links_terms') . ' AS m ON m.link_id = l.link_id')
->where('m.term_id IN (' . implode(',', $included) . ')');
// Check if there are any excluded terms to deal with.
if (count($this->excludedTerms)) {
$query2 = $db->getQuery(true);
$query2->select('e.link_id')
->from($db->quoteName('#__finder_links_terms', 'e'))
->where('e.term_id IN (' . implode(',', $this->excludedTerms) . ')');
$query->where('l.link_id NOT IN (' . $query2 . ')');
}
/*
* The query contains required search terms.
*/
if (count($this->requiredTerms)) {
foreach ($this->requiredTerms as $terms) {
if (count($terms)) {
$query->having('SUM(CASE WHEN m.term_id IN (' . implode(',', $terms) . ') THEN 1 ELSE 0 END) > 0');
} else {
$query->where('false');
break;
}
}
}
return $query;
}
/**
* Method to get the available sorting fields.
*
* @return array The sorting field objects.
*
* @throws \Exception
*
* @since 4.3.0
*/
public function getSortOrderFields()
{
$sortOrderFields = [];
$directions = ['asc', 'desc'];
$app = Factory::getApplication();
$params = $app->getParams();
$sortOrderFieldValues = $params->get('shown_sort_order', [], 'array');
if ($params->get('show_sort_order', 0, 'uint') && !empty($sortOrderFieldValues)) {
$defaultSortFieldValue = $params->get('sort_order', '', 'cmd');
$queryUri = Uri::getInstance($this->getQuery()->toUri());
// If the default field is not included in the shown sort fields, add it.
if (!in_array($defaultSortFieldValue, $sortOrderFieldValues)) {
array_unshift($sortOrderFieldValues, $defaultSortFieldValue);
}
foreach ($sortOrderFieldValues as $sortOrderFieldValue) {
foreach ($directions as $direction) {
// The relevance has only descending direction. Except if ascending is set in the parameters.
if ($sortOrderFieldValue === 'relevance' && $direction === 'asc' && $app->getParams()->get('sort_direction', 'desc') === 'desc') {
continue;
}
$sortOrderFields[] = $this->getSortField($sortOrderFieldValue, $direction, $queryUri);
}
}
}
// Import Finder plugins
PluginHelper::importPlugin('finder');
// Trigger an event, in case a plugin wishes to change the order fields.
$app->triggerEvent('onFinderSortOrderFields', [&$sortOrderFields]);
return $sortOrderFields;
}
/**
* Method to generate and return a sorting field
*
* @param string $value The value based on which the results will be sorted.
* @param string $direction The sorting direction ('asc' or 'desc').
* @param Uri $queryUri The uri of the search query.
*
* @return \stdClass The sorting field object.
*
* @throws \Exception
*
* @since 4.3.0
*/
protected function getSortField(string $value, string $direction, Uri $queryUri)
{
$sortField = new \stdClass();
$app = Factory::getApplication();
// We have to clone the query uri. Otherwise the next elements will use the same.
$queryUri = clone $queryUri;
$queryUri->setVar('o', $value);
$currentOrderingDirection = $app->getInput()->getWord('od', $app->getParams()->get('sort_direction', 'desc'));
// Validate the sorting direction and add it only if it is different than the set in the params.
if (in_array($direction, ['asc', 'desc']) && $direction != $app->getParams()->get('sort_direction', 'desc')) {
$queryUri->setVar('od', StringHelper::strtolower($direction));
}
$label = '';
if (isset($this->sortOrderFieldsLabels[$value . '.' . $direction])) {
$label = Text::_($this->sortOrderFieldsLabels[$value . '.' . $direction]);
}
$sortField->label = $label;
$sortField->url = $queryUri->toString();
$currentSortOrderField = $app->getInput()->getWord('o', $app->getParams()->get('sort_order', 'relevance'));
$sortField->active = false;
if ($value === StringHelper::strtolower($currentSortOrderField) && $direction === $currentOrderingDirection) {
$sortField->active = true;
}
return $sortField;
}
/**
* Method to get a store id based on model the configuration state.
*
* This is necessary because the model is used by the component and
* different modules that might need different sets of data or different
* ordering requirements.
*
* @param string $id An identifier string to generate the store id. [optional]
* @param boolean $page True to store the data paged, false to store all data. [optional]
*
* @return string A store id.
*
* @since 2.5
*/
protected function getStoreId($id = '', $page = true)
{
// Get the query object.
$query = $this->getQuery();
// Add the search query state.
$id .= ':' . $query->input;
$id .= ':' . $query->language;
$id .= ':' . $query->filter;
$id .= ':' . serialize($query->filters);
$id .= ':' . $query->date1;
$id .= ':' . $query->date2;
$id .= ':' . $query->when1;
$id .= ':' . $query->when2;
if ($page) {
// Add the list state for page specific data.
$id .= ':' . $this->getState('list.start');
$id .= ':' . $this->getState('list.limit');
$id .= ':' . $this->getState('list.ordering');
$id .= ':' . $this->getState('list.direction');
}
return parent::getStoreId($id);
}
/**
* Method to auto-populate the model state. Calling getState in this method will result in recursion.
*
* @param string $ordering An optional ordering field. [optional]
* @param string $direction An optional direction. [optional]
*
* @return void
*
* @since 2.5
*/
protected function populateState($ordering = null, $direction = null)
{
// Get the configuration options.
$app = Factory::getApplication();
$input = $app->getInput();
$params = $app->getParams();
$user = $this->getCurrentUser();
$language = $app->getLanguage();
$this->setState('filter.language', Multilanguage::isEnabled());
$request = $input->request;
$options = [];
// Get the empty query setting.
$options['empty'] = $params->get('allow_empty_query', 0);
// Get the static taxonomy filters.
$options['filter'] = $request->getInt('f', $params->get('f', ''));
// Get the dynamic taxonomy filters.
$options['filters'] = $request->get('t', $params->get('t', []), 'array');
// Get the query string.
$options['input'] = $request->getString('q', $params->get('q', ''));
// Get the query language.
$options['language'] = $request->getCmd('l', $params->get('l', $language->getTag()));
// Set the word match mode
$options['word_match'] = $params->get('word_match', 'exact');
// Get the start date and start date modifier filters.
$options['date1'] = $request->getString('d1', $params->get('d1', ''));
$options['when1'] = $request->getString('w1', $params->get('w1', ''));
// Get the end date and end date modifier filters.
$options['date2'] = $request->getString('d2', $params->get('d2', ''));
$options['when2'] = $request->getString('w2', $params->get('w2', ''));
// Load the query object.
$this->searchquery = new Query($options, $this->getDatabase());
// Load the query token data.
$this->excludedTerms = $this->searchquery->getExcludedTermIds();
$this->includedTerms = $this->searchquery->getIncludedTermIds();
$this->requiredTerms = $this->searchquery->getRequiredTermIds();
// Load the list state.
$this->setState('list.start', $input->get('limitstart', 0, 'uint'));
$this->setState('list.limit', $input->get('limit', $params->get('list_limit', $app->get('list_limit', 20)), 'uint'));
/*
* Load the sort ordering.
* Currently this is 'hard' coded via menu item parameter but may not satisfy a users need.
* More flexibility was way more user friendly. So we allow the user to pass a custom value
* from the pool of fields that are indexed like the 'title' field.
* Also, we allow this parameter to be passed in either case (lower/upper).
*/
$order = $input->getWord('o', $params->get('sort_order', 'relevance'));
$order = StringHelper::strtolower($order);
$this->setState('list.raworder', $order);
switch ($order) {
case 'date':
$this->setState('list.ordering', 'l.start_date');
break;
case 'price':
$this->setState('list.ordering', 'l.list_price');
break;
case 'sale_price':
$this->setState('list.ordering', 'l.sale_price');
break;
case ($order === 'relevance' && !empty($this->includedTerms)):
$this->setState('list.ordering', 'm.weight');
break;
case 'title':
$this->setState('list.ordering', 'l.title');
break;
default:
$this->setState('list.ordering', 'l.link_id');
$this->setState('list.raworder');
break;
}
/*
* Load the sort direction.
* Currently this is 'hard' coded via menu item parameter but may not satisfy a users need.
* More flexibility was way more user friendly. So we allow to be inverted.
* Also, we allow this parameter to be passed in either case (lower/upper).
*/
$dirn = $input->getWord('od', $params->get('sort_direction', 'desc'));
$dirn = StringHelper::strtolower($dirn);
switch ($dirn) {
case 'asc':
$this->setState('list.direction', 'ASC');
break;
default:
$this->setState('list.direction', 'DESC');
break;
}
// Set the match limit.
$this->setState('match.limit', 1000);
// Load the parameters.
$this->setState('params', $params);
// Load the user state.
$this->setState('user.id', (int) $user->get('id'));
$this->setState('user.groups', $user->getAuthorisedViewLevels());
}
}