View source
<?php
namespace Drupal\photos\Plugin\Search;
use Drupal\Core\Access\AccessibleInterface;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Config\Config;
use Drupal\Core\Database\Connection;
use Drupal\Core\Database\Query\Condition;
use Drupal\Core\Database\Query\SelectExtender;
use Drupal\Core\Database\StatementInterface;
use Drupal\Core\DependencyInjection\DeprecatedServicePropertyTrait;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\Security\TrustedCallbackInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\photos\Entity\PhotosImage;
use Drupal\search\Plugin\ConfigurableSearchPluginBase;
use Drupal\search\Plugin\SearchIndexingInterface;
use Drupal\search\SearchIndexInterface;
use Drupal\Search\SearchQuery;
use Symfony\Component\DependencyInjection\ContainerInterface;
class PhotosImageSearch extends ConfigurableSearchPluginBase implements AccessibleInterface, SearchIndexingInterface, TrustedCallbackInterface {
use DeprecatedServicePropertyTrait;
protected $deprecatedProperties = [
'entityManager' => 'entity.manager',
];
protected $database;
protected $databaseReplica;
protected $entityTypeManager;
protected $moduleHandler;
protected $searchSettings;
protected $languageManager;
protected $account;
protected $renderer;
protected $searchIndex;
protected $rankings;
protected $advanced = [
'album_id' => [
'column' => 'p.album_id',
],
'language' => [
'column' => 'i.langcode',
],
'author' => [
'column' => 'p.uid',
],
];
const ADVANCED_FORM = 'advanced-form';
protected $messenger;
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static($configuration, $plugin_id, $plugin_definition, $container
->get('database'), $container
->get('entity_type.manager'), $container
->get('module_handler'), $container
->get('config.factory')
->get('search.settings'), $container
->get('language_manager'), $container
->get('renderer'), $container
->get('messenger'), $container
->get('current_user'), $container
->get('database.replica'), $container
->get('search.index'));
}
public function __construct(array $configuration, $plugin_id, $plugin_definition, Connection $database, EntityTypeManagerInterface $entity_type_manager, ModuleHandlerInterface $module_handler, Config $search_settings, LanguageManagerInterface $language_manager, RendererInterface $renderer, MessengerInterface $messenger, AccountInterface $account = NULL, Connection $database_replica = NULL, SearchIndexInterface $search_index = NULL) {
$this->database = $database;
$this->databaseReplica = $database_replica ?: $database;
$this->entityTypeManager = $entity_type_manager;
$this->moduleHandler = $module_handler;
$this->searchSettings = $search_settings;
$this->languageManager = $language_manager;
$this->renderer = $renderer;
$this->messenger = $messenger;
$this->account = $account;
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this
->addCacheTags([
'photos_image_list',
]);
$this->searchIndex = $search_index;
}
public function access($operation = 'view', AccountInterface $account = NULL, $return_as_object = FALSE) {
$result = AccessResult::allowedIfHasPermission($account, 'view photo');
return $return_as_object ? $result : $result
->isAllowed();
}
public function isSearchExecutable() {
return !empty($this->keywords) || isset($this->searchParameters['f']) && count($this->searchParameters['f']);
}
public function getType() {
return $this
->getPluginId();
}
public function execute() {
if ($this
->isSearchExecutable()) {
$results = $this
->findResults();
if ($results) {
return $this
->prepareResults($results);
}
}
return [];
}
protected function findResults() {
$keys = $this->keywords;
$query = $this->databaseReplica
->select('search_index', 'i')
->extend('Drupal\\search\\SearchQuery')
->extend('Drupal\\Core\\Database\\Query\\PagerSelectExtender');
$query
->join('photos_image_field_data', 'p', 'p.id = i.sid AND p.langcode = i.langcode');
$query
->join('node_field_data', 'n', 'n.nid = p.album_id AND n.langcode = i.langcode');
$query
->condition('p.status', 1);
$query
->condition('n.status', 1)
->addTag('node_access')
->searchExpression($keys, $this
->getPluginId());
$parameters = $this
->getParameters();
if (!empty($parameters['f']) && is_array($parameters['f'])) {
$filters = [];
$pattern = '/^(' . implode('|', array_keys($this->advanced)) . '):([^ ]*)/i';
foreach ($parameters['f'] as $item) {
if (preg_match($pattern, $item, $m)) {
$filters[$m[1]][$m[2]] = $m[2];
}
}
foreach ($filters as $option => $matched) {
$info = $this->advanced[$option];
$operator = empty($info['operator']) ? 'OR' : $info['operator'];
$where = new Condition($operator);
foreach ($matched as $value) {
$where
->condition($info['column'], $value);
}
$query
->condition($where);
if (!empty($info['join'])) {
$query
->join($info['join']['table'], $info['join']['alias'], $info['join']['condition']);
}
}
}
$this
->addPhotosImageRankings($query);
$find = $query
->fields('i', [
'langcode',
])
->groupBy('i.langcode')
->limit(10)
->execute();
$status = $query
->getStatus();
if ($status & SearchQuery::EXPRESSIONS_IGNORED) {
$this->messenger
->addWarning($this
->t('Your search used too many AND/OR expressions. Only the first @count terms were included in this search.', [
'@count' => $this->searchSettings
->get('and_or_limit'),
]));
}
if ($status & SearchQuery::LOWER_CASE_OR) {
$this->messenger
->addWarning($this
->t('Search for either of the two terms with uppercase <strong>OR</strong>. For example, <strong>cats OR dogs</strong>.'));
}
if ($status & SearchQuery::NO_POSITIVE_KEYWORDS) {
$this->messenger
->addWarning($this
->formatPlural($this->searchSettings
->get('index.minimum_word_size'), 'You must include at least one keyword to match in the content, and punctuation is ignored.', 'You must include at least one keyword to match in the content. Keywords must be at least @count characters, and punctuation is ignored.'));
}
return $find;
}
protected function prepareResults(StatementInterface $found) {
$results = [];
$nodeStorage = $this->entityTypeManager
->getStorage('node');
$nodeRender = $this->entityTypeManager
->getViewBuilder('node');
$photosImageStorage = $this->entityTypeManager
->getStorage('photos_image');
$photosImageRender = $this->entityTypeManager
->getViewBuilder('photos_image');
$keys = $this->keywords;
foreach ($found as $item) {
$photosImage = $photosImageStorage
->load($item->sid)
->getTranslation($item->langcode);
$build = $photosImageRender
->view($photosImage, 'search_result', $item->langcode);
$node = $nodeStorage
->load($photosImage
->getAlbumId())
->getTranslation($item->langcode);
unset($build['#theme']);
$build['#pre_render'][] = [
$this,
'removeSubmittedInfo',
];
$rendered = $this->renderer
->renderPlain($build);
$this
->addCacheableDependency(CacheableMetadata::createFromRenderArray($build));
$rendered .= ' ' . $this->moduleHandler
->invoke('comment', 'photos_image_update_index', [
$photosImage,
]);
$extra = $this->moduleHandler
->invokeAll('photos_image_search_result', [
$photosImage,
]);
$username = [
'#theme' => 'username',
'#account' => $photosImage
->getOwner(),
];
$result = [
'link' => $photosImage
->toUrl('canonical', [
'absolute' => TRUE,
])
->toString(),
'album_title' => $node
->label(),
'title' => $photosImage
->label(),
'node' => $node,
'photos_image' => $photosImage,
'extra' => $extra,
'score' => $item->calculated_score,
'snippet' => search_excerpt($keys, $rendered, $item->langcode),
'langcode' => $photosImage
->language()
->getId(),
];
$this
->addCacheableDependency($photosImage);
$this
->addCacheableDependency($photosImage
->getOwner());
$result += [
'user' => $this->renderer
->renderPlain($username),
'date' => $photosImage
->getChangedTime(),
];
$results[] = $result;
}
return $results;
}
public function removeSubmittedInfo(array $build) {
unset($build['created']);
unset($build['uid']);
return $build;
}
protected function addPhotosImageRankings(SelectExtender $query) {
if ($ranking = $this
->getRankings()) {
$tables =& $query
->getTables();
foreach ($ranking as $rank => $values) {
if (isset($this->configuration['rankings'][$rank]) && !empty($this->configuration['rankings'][$rank])) {
$photos_image_rank = $this->configuration['rankings'][$rank];
if (isset($values['join']) && !isset($tables[$values['join']['alias']])) {
$query
->addJoin($values['join']['type'], $values['join']['table'], $values['join']['alias'], $values['join']['on']);
}
$arguments = isset($values['arguments']) ? $values['arguments'] : [];
$query
->addScore($values['score'], $arguments, $photos_image_rank);
}
}
}
}
public function updateIndex() {
$limit = (int) $this->searchSettings
->get('index.cron_limit');
$query = $this->databaseReplica
->select('photos_image', 'p');
$query
->addField('p', 'id');
$query
->leftJoin('search_dataset', 'sd', 'sd.sid = p.id AND sd.type = :type', [
':type' => $this
->getPluginId(),
]);
$query
->addExpression('CASE MAX(sd.reindex) WHEN NULL THEN 0 ELSE 1 END', 'ex');
$query
->addExpression('MAX(sd.reindex)', 'ex2');
$query
->condition($query
->orConditionGroup()
->where('sd.sid IS NULL')
->condition('sd.reindex', 0, '<>'));
$query
->orderBy('ex', 'DESC')
->orderBy('ex2')
->orderBy('p.id')
->groupBy('p.id')
->range(0, $limit);
$photosImageIds = $query
->execute()
->fetchCol();
if (!$photosImageIds) {
return;
}
$photosImageStorage = $this->entityTypeManager
->getStorage('photos_image');
$words = [];
try {
foreach ($photosImageStorage
->loadMultiple($photosImageIds) as $photosImage) {
$words += $this
->indexPhotosImage($photosImage);
}
} finally {
$this->searchIndex
->updateWordWeights($words);
}
}
protected function indexPhotosImage(PhotosImage $photosImage) {
$words = [];
$languages = $photosImage
->getTranslationLanguages();
$node_render = $this->entityTypeManager
->getViewBuilder('photos_image');
foreach ($languages as $language) {
$photosImage = $photosImage
->getTranslation($language
->getId());
$build = $node_render
->view($photosImage, 'search_index', $language
->getId());
unset($build['#theme']);
$build['search_title'] = [
'#prefix' => '<h1>',
'#plain_text' => $photosImage
->label(),
'#suffix' => '</h1>',
'#weight' => -1000,
];
$text = $this->renderer
->renderPlain($build);
$extra = $this->moduleHandler
->invokeAll('node_update_index', [
$photosImage,
]);
foreach ($extra as $t) {
$text .= $t;
}
$words += $this->searchIndex
->index($this
->getPluginId(), $photosImage
->id(), $language
->getId(), $text, FALSE);
}
return $words;
}
public function indexClear() {
$this->searchIndex
->clear($this
->getPluginId());
}
public function markForReindex() {
$this->searchIndex
->markForReindex($this
->getPluginId());
}
public function indexStatus() {
$total = $this->database
->query('SELECT COUNT(*) FROM {photos_image}')
->fetchField();
$remaining = $this->database
->query("SELECT COUNT(DISTINCT p.id) FROM {photos_image} p LEFT JOIN {search_dataset} sd ON sd.sid = p.id AND sd.type = :type WHERE sd.sid IS NULL OR sd.reindex <> 0", [
':type' => $this
->getPluginId(),
])
->fetchField();
return [
'remaining' => $remaining,
'total' => $total,
];
}
public function searchFormAlter(array &$form, FormStateInterface $form_state) {
$parameters = $this
->getParameters();
$keys = $this
->getKeywords();
$used_advanced = !empty($parameters[self::ADVANCED_FORM]);
if ($used_advanced) {
$f = isset($parameters['f']) ? (array) $parameters['f'] : [];
$defaults = $this
->parseAdvancedDefaults($f, $keys);
}
else {
$defaults = [
'keys' => $keys,
];
}
$form['basic']['keys']['#default_value'] = $defaults['keys'];
$form['advanced'] = [
'#type' => 'details',
'#title' => t('Advanced search'),
'#attributes' => [
'class' => [
'search-advanced',
],
],
'#access' => $this->account && $this->account
->hasPermission('use advanced search'),
'#open' => $used_advanced,
];
$form['advanced']['keywords-fieldset'] = [
'#type' => 'fieldset',
'#title' => t('Keywords'),
];
$form['advanced']['keywords'] = [
'#prefix' => '<div class="criterion">',
'#suffix' => '</div>',
];
$form['advanced']['keywords-fieldset']['keywords']['or'] = [
'#type' => 'textfield',
'#title' => t('Containing any of the words'),
'#size' => 30,
'#maxlength' => 255,
'#default_value' => isset($defaults['or']) ? $defaults['or'] : '',
];
$form['advanced']['keywords-fieldset']['keywords']['phrase'] = [
'#type' => 'textfield',
'#title' => t('Containing the phrase'),
'#size' => 30,
'#maxlength' => 255,
'#default_value' => isset($defaults['phrase']) ? $defaults['phrase'] : '',
];
$form['advanced']['keywords-fieldset']['keywords']['negative'] = [
'#type' => 'textfield',
'#title' => t('Containing none of the words'),
'#size' => 30,
'#maxlength' => 255,
'#default_value' => isset($defaults['negative']) ? $defaults['negative'] : '',
];
$albums = array_map([
'\\Drupal\\Component\\Utility\\Html',
'escape',
], $this
->getAlbumNames());
$form['advanced']['albums-fieldset'] = [
'#type' => 'fieldset',
'#title' => t('Albums'),
];
$form['advanced']['albums-fieldset']['album_id'] = [
'#type' => 'checkboxes',
'#title' => t('Only in the selected album(s)'),
'#prefix' => '<div class="criterion">',
'#suffix' => '</div>',
'#options' => $albums,
'#default_value' => isset($defaults['album_id']) ? $defaults['album_id'] : [],
];
$form['advanced']['submit'] = [
'#type' => 'submit',
'#value' => t('Advanced search'),
'#prefix' => '<div class="action">',
'#suffix' => '</div>',
'#weight' => 100,
];
$language_options = [];
$language_list = $this->languageManager
->getLanguages(LanguageInterface::STATE_ALL);
foreach ($language_list as $langcode => $language) {
$language_options[$langcode] = $language
->isLocked() ? t('- @name -', [
'@name' => $language
->getName(),
]) : $language
->getName();
}
if (count($language_options) > 1) {
$form['advanced']['lang-fieldset'] = [
'#type' => 'fieldset',
'#title' => t('Languages'),
];
$form['advanced']['lang-fieldset']['language'] = [
'#type' => 'checkboxes',
'#title' => t('Languages'),
'#prefix' => '<div class="criterion">',
'#suffix' => '</div>',
'#options' => $language_options,
'#default_value' => isset($defaults['language']) ? $defaults['language'] : [],
];
}
}
public function getAlbumNames() {
$options = [];
$results = $this->database
->select('node_field_data', 'n')
->fields('n', [
'nid',
'title',
])
->condition('type', 'photos')
->condition('status', 1)
->addTag('node_access')
->execute();
foreach ($results as $result) {
$options[$result->nid] = $result->title;
}
return $options;
}
public function buildSearchUrlQuery(FormStateInterface $form_state) {
$keys = trim($form_state
->getValue('keys'));
$advanced = FALSE;
$filters = [];
if ($form_state
->hasValue('album_id') && is_array($form_state
->getValue('album_id'))) {
foreach ($form_state
->getValue('album_id') as $albumId) {
if ($albumId) {
$advanced = TRUE;
$filters[] = 'album_id:' . $albumId;
}
}
}
if ($form_state
->hasValue('language') && is_array($form_state
->getValue('language'))) {
foreach ($form_state
->getValue('language') as $language) {
if ($language) {
$advanced = TRUE;
$filters[] = 'language:' . $language;
}
}
}
if ($form_state
->getValue('or') != '') {
if (preg_match_all('/ ("[^"]+"|[^" ]+)/i', ' ' . $form_state
->getValue('or'), $matches)) {
$keys .= ' ' . implode(' OR ', $matches[1]);
$advanced = TRUE;
}
}
if ($form_state
->getValue('negative') != '') {
if (preg_match_all('/ ("[^"]+"|[^" ]+)/i', ' ' . $form_state
->getValue('negative'), $matches)) {
$keys .= ' -' . implode(' -', $matches[1]);
$advanced = TRUE;
}
}
if ($form_state
->getValue('phrase') != '') {
$keys .= ' "' . str_replace('"', ' ', $form_state
->getValue('phrase')) . '"';
$advanced = TRUE;
}
$keys = trim($keys);
$query = [
'keys' => $keys,
];
if ($filters) {
$query['f'] = $filters;
}
if ($advanced) {
$query[self::ADVANCED_FORM] = '1';
}
return $query;
}
protected function parseAdvancedDefaults(array $f, $keys) {
$defaults = [];
foreach ($f as $advanced) {
[
$key,
$value,
] = explode(':', $advanced, 2);
if (!isset($defaults[$key])) {
$defaults[$key] = [];
}
$defaults[$key][] = $value;
}
$matches = [];
$keys = ' ' . $keys . ' ';
if (preg_match('/ "([^"]+)" /', $keys, $matches)) {
$keys = str_replace($matches[0], ' ', $keys);
$defaults['phrase'] = $matches[1];
}
if (preg_match_all('/ -([^ ]+)/', $keys, $matches)) {
$keys = str_replace($matches[0], ' ', $keys);
$defaults['negative'] = implode(' ', $matches[1]);
}
if (preg_match('/ [^ ]+( OR [^ ]+)+ /', $keys, $matches)) {
$keys = str_replace($matches[0], ' ', $keys);
$words = explode(' OR ', trim($matches[0]));
$defaults['or'] = implode(' ', $words);
}
$defaults['keys'] = trim($keys);
return $defaults;
}
protected function getRankings() {
if (!$this->rankings) {
$this->rankings = $this->moduleHandler
->invokeAll('ranking');
}
return $this->rankings;
}
public function defaultConfiguration() {
$configuration = [
'rankings' => [],
];
return $configuration;
}
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
$form['content_ranking'] = [
'#type' => 'details',
'#title' => t('Content ranking'),
'#open' => TRUE,
];
$form['content_ranking']['info'] = [
'#markup' => '<p><em>' . $this
->t('Influence is a numeric multiplier used in ordering search results. A higher number means the corresponding factor has more influence on search results; zero means the factor is ignored. Changing these numbers does not require the search index to be rebuilt. Changes take effect immediately.') . '</em></p>',
];
$header = [
$this
->t('Factor'),
$this
->t('Influence'),
];
$form['content_ranking']['rankings'] = [
'#type' => 'table',
'#header' => $header,
];
$range = range(0, 10);
$options = array_combine($range, $range);
foreach ($this
->getRankings() as $var => $values) {
$form['content_ranking']['rankings'][$var]['name'] = [
'#markup' => $values['title'],
];
$form['content_ranking']['rankings'][$var]['value'] = [
'#type' => 'select',
'#options' => $options,
'#attributes' => [
'aria-label' => $this
->t("Influence of '@title'", [
'@title' => $values['title'],
]),
],
'#default_value' => isset($this->configuration['rankings'][$var]) ? $this->configuration['rankings'][$var] : 0,
];
}
return $form;
}
public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
foreach ($this
->getRankings() as $var => $values) {
if (!$form_state
->isValueEmpty([
'rankings',
$var,
'value',
])) {
$this->configuration['rankings'][$var] = $form_state
->getValue([
'rankings',
$var,
'value',
]);
}
else {
unset($this->configuration['rankings'][$var]);
}
}
}
public static function trustedCallbacks() {
return [
'removeSubmittedInfo',
];
}
}