Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
157 changes: 124 additions & 33 deletions core/src/Revolution/Processors/Search/Search.php
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<?php

/*
* This file is part of the MODX Revolution package.
*
Expand All @@ -10,7 +11,6 @@

namespace MODX\Revolution\Processors\Search;


use MODX\Revolution\modChunk;
use MODX\Revolution\modContext;
use MODX\Revolution\modElement;
Expand All @@ -20,6 +20,7 @@
use MODX\Revolution\modSnippet;
use MODX\Revolution\modTemplate;
use MODX\Revolution\modTemplateVar;
use MODX\Revolution\modTemplateVarResource;
use MODX\Revolution\modUser;
use MODX\Revolution\modUserProfile;

Expand Down Expand Up @@ -100,55 +101,145 @@ public function process()
}

/**
* Search in resources
* Returns context keys for resource search (excluding mgr).
*
* @return array<int, string>
*/
protected function searchResources()
protected function getResourceContextKeys(): array
{
$contextKeys = [];
$contexts = $this->modx->getIterator(modContext::class, ['key:!=' => 'mgr']);
foreach ($contexts as $context) {
$contextKeys[] = $context->get('key');
}
return $contextKeys;
}

$c = $this->modx->newQuery(modResource::class);
$c->leftJoin(modTemplate::class, 'modTemplate', 'modResource.template = modTemplate.id');
$c->select($this->modx->getSelectColumns(modResource::class, 'modResource'));
$c->select('modTemplate.icon as icon');
/**
* Returns resource IDs that have the search query in any TV value.
*
* @return array<int, int>
*/
protected function getResourceIdsMatchingTvValues(): array
{
if (!$this->searchInContent()) {
return [];
}

$escaped = addcslashes($this->query, '%_');
$c = $this->modx->newQuery(modTemplateVarResource::class);
$c->select('contentid');
$c->where(['value:LIKE' => '%' . $escaped . '%']);
$c->groupby('contentid');
$c->limit($this->getMaxResults() * 2);

$ids = [];
foreach ($this->modx->getIterator(modTemplateVarResource::class, $c) as $row) {
$ids[] = (int) $row->get('contentid');
}

return array_values(array_unique($ids));
}

/**
* Builds search criteria and context for resource query, including TV-matched IDs.
*
* @param array<int, string> $contextKeys
* @return array{search: array, context: array, tvIds: array<int, int>}
*/
protected function buildResourceSearchCriteria(array $contextKeys): array
{
$querySearch = [
'modResource.pagetitle:LIKE' => '%' . $this->query .'%',
'OR:modResource.longtitle:LIKE' => '%' . $this->query .'%',
'OR:modResource.alias:LIKE' => '%' . $this->query .'%',
'OR:modResource.description:LIKE' => '%' . $this->query .'%',
'OR:modResource.introtext:LIKE' => '%' . $this->query .'%',
'modResource.pagetitle:LIKE' => '%' . $this->query . '%',
'OR:modResource.longtitle:LIKE' => '%' . $this->query . '%',
'OR:modResource.alias:LIKE' => '%' . $this->query . '%',
'OR:modResource.description:LIKE' => '%' . $this->query . '%',
'OR:modResource.introtext:LIKE' => '%' . $this->query . '%',
];
$tvIds = [];
if ($this->searchInContent()) {
$querySearch['OR:modResource.content:LIKE'] = '%' . $this->query .'%';
$querySearch['OR:modResource.content:LIKE'] = '%' . $this->query . '%';
$tvIds = $this->getResourceIdsMatchingTvValues();
if (!empty($tvIds)) {
$querySearch['OR:modResource.id:IN'] = $tvIds;
}
}
$querySearch['OR:modResource.id:='] = $this->query;
$queryContext = [
'modResource.context_key:IN' => $contextKeys,
];
$c->where($querySearch, $queryContext);

$c->sortby('IF(`modResource`.`pagetitle` = ' . $this->modx->quote($this->query) . ', 0, 1)');
return ['search' => $querySearch, 'context' => $queryContext, 'tvIds' => $tvIds];
}

/**
* Applies relevance-based sort order to the resource search query.
* Sort levels must stay in sync with fields in buildResourceSearchCriteria().
*
* @param \xPDO\Om\xPDOQuery $c
* @param array<int, int> $tvIds
*/
protected function applyResourceSearchSortBy(\xPDO\Om\xPDOQuery $c, array $tvIds): void
{
$q = $this->modx->quote($this->query);
$qLike = $this->modx->quote($this->query . '%');
$qContains = $this->modx->quote('%' . $this->query . '%');

$c->sortby('(`modResource`.`pagetitle` = ' . $q . ')', 'DESC');
$c->sortby('(`modResource`.`pagetitle` LIKE ' . $qLike . ')', 'DESC');
$otherFieldsLike = '(`modResource`.`longtitle` LIKE ' . $qContains
. ' OR `modResource`.`alias` LIKE ' . $qContains
. ' OR `modResource`.`description` LIKE ' . $qContains
. ' OR `modResource`.`introtext` LIKE ' . $qContains . ')';
$c->sortby($otherFieldsLike, 'DESC');
if ($this->searchInContent()) {
$c->sortby('(`modResource`.`content` LIKE ' . $qContains . ')', 'DESC');
if (!empty($tvIds)) {
$ids = implode(',', array_map('intval', $tvIds));
$c->sortby('(`modResource`.`id` IN (' . $ids . '))', 'DESC');
}
}
$c->sortby('modResource.createdon', 'DESC');
}

/**
* Formats a resource record for the search results array.
*
* @param modResource $record
* @return array<string, mixed>
*/
protected function formatResourceSearchResult(modResource $record): array
{
return [
'name' => $this->modx->hasPermission('tree_show_resource_ids')
? $record->get('pagetitle') . ' (' . $record->get('id') . ')'
: $record->get('pagetitle'),
'_action' => 'resource/update&id=' . $record->get('id'),
'description' => $record->get('description'),
'type' => static::TYPE_RESOURCE . 's',
'class' => $record->get('class_key'),
'icon' => str_replace('icon-', '', $record->get('icon')),
];
}

/**
* Search in resources
*/
protected function searchResources()
{
$contextKeys = $this->getResourceContextKeys();
$criteria = $this->buildResourceSearchCriteria($contextKeys);

$c = $this->modx->newQuery(modResource::class);
$c->leftJoin(modTemplate::class, 'modTemplate', 'modResource.template = modTemplate.id');
$c->select($this->modx->getSelectColumns(modResource::class, 'modResource'));
$c->select('modTemplate.icon as icon');
$c->where($criteria['search'], $criteria['context']);
$this->applyResourceSearchSortBy($c, $criteria['tvIds']);
$c->limit($this->getMaxResults());

$collection = $this->modx->getIterator(modResource::class, $c);
/** @var modResource $record */
foreach ($collection as $record) {
$this->results[] = [
'name' => $this->modx->hasPermission('tree_show_resource_ids')
? $record->get('pagetitle') . ' (' . $record->get('id') . ')'
: $record->get('pagetitle'),
'_action' => 'resource/update&id=' . $record->get('id'),
'description' => $record->get('description'),
'type' => static::TYPE_RESOURCE . 's',
'class' => $record->get('class_key'),
'icon' => str_replace('icon-', '', $record->get('icon'))
];
foreach ($this->modx->getIterator(modResource::class, $c) as $record) {
$this->results[] = $this->formatResourceSearchResult($record);
}
}

Expand All @@ -165,10 +256,10 @@ protected function searchElements($class, $type = '', $nameField = 'name', $desc
$c = $this->modx->newQuery($class);
$querySearch = [
$nameField . ':LIKE' => '%' . $this->query . '%',
'OR:' . $descriptionField . ':LIKE' => '%' . $this->query .'%',
'OR:' . $descriptionField . ':LIKE' => '%' . $this->query . '%',
];
if ($this->searchInContent() && !empty($contentField)) {
$querySearch['OR:' . $contentField . ':LIKE'] = '%' . $this->query .'%';
$querySearch['OR:' . $contentField . ':LIKE'] = '%' . $this->query . '%';
}
$querySearch['OR:id:='] = $this->query;
$c->where($querySearch);
Expand Down Expand Up @@ -203,8 +294,8 @@ protected function searchUsers()
$c->leftJoin(modUserProfile::class, 'Profile');
$c->where([
'username:LIKE' => '%' . $this->query . '%',
'OR:Profile.fullname:LIKE' => '%' . $this->query .'%',
'OR:Profile.email:LIKE' => '%' . $this->query .'%',
'OR:Profile.fullname:LIKE' => '%' . $this->query . '%',
'OR:Profile.email:LIKE' => '%' . $this->query . '%',
'OR:id:=' => $this->query,
]);

Expand All @@ -218,7 +309,7 @@ protected function searchUsers()
foreach ($collection as $record) {
$this->results[] = [
'name' => $record->get('username'),
'description' => $record->get('fullname') .' / '. $record->get('email'),
'description' => $record->get('fullname') . ' / ' . $record->get('email'),
'_action' => 'security/user/update&id=' . $record->get('internalKey'),
'type' => static::TYPE_USER . 's',
];
Expand Down