vendor/contao/core-bundle/src/Resources/contao/modules/ModuleSearch.php line 167

Open in your IDE?
  1. <?php
  2. /*
  3.  * This file is part of Contao.
  4.  *
  5.  * (c) Leo Feyer
  6.  *
  7.  * @license LGPL-3.0-or-later
  8.  */
  9. namespace Contao;
  10. use Contao\CoreBundle\Exception\PageNotFoundException;
  11. use Contao\CoreBundle\File\Metadata;
  12. use Contao\CoreBundle\Security\ContaoCorePermissions;
  13. use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
  14. /**
  15.  * Front end module "search".
  16.  */
  17. class ModuleSearch extends Module
  18. {
  19.     /**
  20.      * Template
  21.      * @var string
  22.      */
  23.     protected $strTemplate = 'mod_search';
  24.     /**
  25.      * Display a wildcard in the back end
  26.      *
  27.      * @return string
  28.      */
  29.     public function generate()
  30.     {
  31.         $request = System::getContainer()->get('request_stack')->getCurrentRequest();
  32.         if ($request && System::getContainer()->get('contao.routing.scope_matcher')->isBackendRequest($request))
  33.         {
  34.             $objTemplate = new BackendTemplate('be_wildcard');
  35.             $objTemplate->wildcard = '### ' . $GLOBALS['TL_LANG']['FMD']['search'][0] . ' ###';
  36.             $objTemplate->title = $this->headline;
  37.             $objTemplate->id = $this->id;
  38.             $objTemplate->link = $this->name;
  39.             $objTemplate->href = StringUtil::specialcharsUrl(System::getContainer()->get('router')->generate('contao_backend', array('do'=>'themes', 'table'=>'tl_module', 'act'=>'edit', 'id'=>$this->id)));
  40.             return $objTemplate->parse();
  41.         }
  42.         $this->pages = StringUtil::deserialize($this->pages);
  43.         return parent::generate();
  44.     }
  45.     /**
  46.      * Generate the module
  47.      */
  48.     protected function compile()
  49.     {
  50.         // Mark the x and y parameter as used (see #4277)
  51.         if (isset($_GET['x']))
  52.         {
  53.             Input::get('x');
  54.             Input::get('y');
  55.         }
  56.         // Trigger the search module from a custom form
  57.         if (!isset($_GET['keywords']) && Input::post('FORM_SUBMIT') == 'tl_search')
  58.         {
  59.             $_GET['keywords'] = Input::post('keywords');
  60.             $_GET['query_type'] = Input::post('query_type');
  61.             $_GET['per_page'] = Input::post('per_page');
  62.         }
  63.         $blnFuzzy = $this->fuzzy;
  64.         $strQueryType = Input::get('query_type') ?: $this->queryType;
  65.         if (\is_array(Input::get('keywords')))
  66.         {
  67.             throw new BadRequestHttpException('Expected string, got array');
  68.         }
  69.         $strKeywords = trim(Input::get('keywords'));
  70.         $this->Template->uniqueId = $this->id;
  71.         $this->Template->queryType = $strQueryType;
  72.         $this->Template->keyword = StringUtil::specialchars($strKeywords);
  73.         $this->Template->keywordLabel = $GLOBALS['TL_LANG']['MSC']['keywords'];
  74.         $this->Template->optionsLabel = $GLOBALS['TL_LANG']['MSC']['options'];
  75.         $this->Template->search = StringUtil::specialchars($GLOBALS['TL_LANG']['MSC']['searchLabel']);
  76.         $this->Template->matchAll = StringUtil::specialchars($GLOBALS['TL_LANG']['MSC']['matchAll']);
  77.         $this->Template->matchAny = StringUtil::specialchars($GLOBALS['TL_LANG']['MSC']['matchAny']);
  78.         $this->Template->advanced = ($this->searchType == 'advanced');
  79.         // Redirect page
  80.         if (($objTarget = $this->objModel->getRelated('jumpTo')) instanceof PageModel)
  81.         {
  82.             /** @var PageModel $objTarget */
  83.             $this->Template->action = $objTarget->getFrontendUrl();
  84.         }
  85.         $this->Template->pagination = '';
  86.         $this->Template->results = '';
  87.         // Execute the search if there are keywords
  88.         if ($strKeywords !== '' && $strKeywords != '*' && !$this->jumpTo)
  89.         {
  90.             // Search pages
  91.             if (!empty($this->pages) && \is_array($this->pages))
  92.             {
  93.                 $arrPages = array();
  94.                 foreach ($this->pages as $intPageId)
  95.                 {
  96.                     $arrPages[] = array($intPageId);
  97.                     $arrPages[] = $this->Database->getChildRecords($intPageId, 'tl_page');
  98.                 }
  99.                 if (!empty($arrPages))
  100.                 {
  101.                     $arrPages = array_merge(...$arrPages);
  102.                 }
  103.                 $arrPages = array_unique($arrPages);
  104.             }
  105.             // Website root
  106.             else
  107.             {
  108.                 /** @var PageModel $objPage */
  109.                 global $objPage;
  110.                 $arrPages = $this->Database->getChildRecords($objPage->rootId, 'tl_page');
  111.             }
  112.             // HOOK: add custom logic (see #5223)
  113.             if (isset($GLOBALS['TL_HOOKS']['customizeSearch']) && \is_array($GLOBALS['TL_HOOKS']['customizeSearch']))
  114.             {
  115.                 foreach ($GLOBALS['TL_HOOKS']['customizeSearch'] as $callback)
  116.                 {
  117.                     $this->import($callback[0]);
  118.                     $this->{$callback[0]}->{$callback[1]}($arrPages, $strKeywords, $strQueryType, $blnFuzzy, $this);
  119.                 }
  120.             }
  121.             // Return if there are no pages
  122.             if (empty($arrPages) || !\is_array($arrPages))
  123.             {
  124.                 return;
  125.             }
  126.             $query_starttime = microtime(true);
  127.             try
  128.             {
  129.                 $objResult = Search::query($strKeywords, ($strQueryType == 'or'), $arrPages, $blnFuzzy, $this->minKeywordLength);
  130.             }
  131.             catch (\Exception $e)
  132.             {
  133.                 System::getContainer()->get('monolog.logger.contao.error')->error('Website search failed: ' . $e->getMessage());
  134.                 $objResult = new SearchResult(array());
  135.             }
  136.             $query_endtime = microtime(true);
  137.             // Sort out protected pages
  138.             if (Config::get('indexProtected'))
  139.             {
  140.                 $objResult->applyFilter(static function ($v)
  141.                 {
  142.                     return empty($v['protected']) || System::getContainer()->get('security.helper')->isGranted(ContaoCorePermissions::MEMBER_IN_GROUPS, StringUtil::deserialize($v['groups'] ?? null, true));
  143.                 });
  144.             }
  145.             $count = $objResult->getCount();
  146.             $this->Template->count = $count;
  147.             $this->Template->page = null;
  148.             $this->Template->keywords = $strKeywords;
  149.             if ($this->minKeywordLength > 0)
  150.             {
  151.                 $this->Template->keywordHint = sprintf($GLOBALS['TL_LANG']['MSC']['sKeywordHint'], $this->minKeywordLength);
  152.             }
  153.             // No results
  154.             if ($count < 1)
  155.             {
  156.                 $this->Template->header = sprintf($GLOBALS['TL_LANG']['MSC']['sEmpty'], $strKeywords);
  157.                 $this->Template->duration = System::getFormattedNumber($query_endtime - $query_starttime, 3) . ' ' . $GLOBALS['TL_LANG']['MSC']['seconds'];
  158.                 return;
  159.             }
  160.             $from = 1;
  161.             $to = $count;
  162.             // Pagination
  163.             if ($this->perPage > 0)
  164.             {
  165.                 $id = 'page_s' . $this->id;
  166.                 $page = (int) (Input::get($id) ?? 1);
  167.                 $per_page = (int) Input::get('per_page') ?: $this->perPage;
  168.                 // Do not index or cache the page if the page number is outside the range
  169.                 if ($page < 1 || $page > max(ceil($count/$per_page), 1))
  170.                 {
  171.                     throw new PageNotFoundException('Page not found: ' . Environment::get('uri'));
  172.                 }
  173.                 $from = (($page - 1) * $per_page) + 1;
  174.                 $to = (($from + $per_page) > $count) ? $count : ($from + $per_page - 1);
  175.                 // Pagination menu
  176.                 if ($to < $count || $from > 1)
  177.                 {
  178.                     $objPagination = new Pagination($count, $per_page, Config::get('maxPaginationLinks'), $id);
  179.                     $this->Template->pagination = $objPagination->generate("\n  ");
  180.                 }
  181.                 $this->Template->page = $page;
  182.             }
  183.             $contextLength = 48;
  184.             $totalLength = 360;
  185.             $lengths = StringUtil::deserialize($this->contextLength, true) + array(null, null);
  186.             if ($lengths[0] > 0)
  187.             {
  188.                 $contextLength = $lengths[0];
  189.             }
  190.             if ($lengths[1] > 0)
  191.             {
  192.                 $totalLength = $lengths[1];
  193.             }
  194.             $arrResult = $objResult->getResults($to-$from+1, $from-1);
  195.             // Get the results
  196.             foreach (array_keys($arrResult) as $i)
  197.             {
  198.                 $objTemplate = new FrontendTemplate($this->searchTpl ?: 'search_default');
  199.                 $objTemplate->setData($arrResult[$i]);
  200.                 $objTemplate->href = $arrResult[$i]['url'];
  201.                 $objTemplate->link = $arrResult[$i]['title'];
  202.                 $objTemplate->url = StringUtil::specialchars(urldecode($arrResult[$i]['url']), true, true);
  203.                 $objTemplate->title = StringUtil::specialchars(StringUtil::stripInsertTags($arrResult[$i]['title']));
  204.                 $objTemplate->class = ($i == 0 ? 'first ' : '') . ((empty($arrResult[$i+1])) ? 'last ' : '') . (($i % 2 == 0) ? 'even' : 'odd');
  205.                 $objTemplate->relevance = sprintf($GLOBALS['TL_LANG']['MSC']['relevance'], number_format($arrResult[$i]['relevance'] / $arrResult[0]['relevance'] * 100, 2) . '%');
  206.                 $objTemplate->unit = $GLOBALS['TL_LANG']['UNITS'][1];
  207.                 $arrContext = array();
  208.                 $strText = StringUtil::stripInsertTags(strtok($arrResult[$i]['text'], "\n"));
  209.                 $arrMatches = Search::getMatchVariants(StringUtil::trimsplit(',', $arrResult[$i]['matches']), $strText, $GLOBALS['TL_LANGUAGE']);
  210.                 // Get the context
  211.                 foreach ($arrMatches as $strWord)
  212.                 {
  213.                     $arrChunks = array();
  214.                     preg_match_all('/(^|(?:\b|^).{0,' . $contextLength . '}(?:\PL|\p{Hiragana}|\p{Katakana}|\p{Han}|\p{Myanmar}|\p{Khmer}|\p{Lao}|\p{Thai}|\p{Tibetan}))' . preg_quote($strWord, '/') . '((?:\PL|\p{Hiragana}|\p{Katakana}|\p{Han}|\p{Myanmar}|\p{Khmer}|\p{Lao}|\p{Thai}|\p{Tibetan}).{0,' . $contextLength . '}(?:\b|$)|$)/ui', $strText, $arrChunks);
  215.                     foreach ($arrChunks[0] as $strContext)
  216.                     {
  217.                         $arrContext[] = ' ' . $strContext . ' ';
  218.                     }
  219.                     // Skip other terms if the total length is already reached
  220.                     if (array_sum(array_map('mb_strlen', $arrContext)) >= $totalLength)
  221.                     {
  222.                         break;
  223.                     }
  224.                 }
  225.                 // Shorten the context and highlight all keywords
  226.                 if (!empty($arrContext))
  227.                 {
  228.                     $objTemplate->context = trim(StringUtil::substrHtml(implode('…', $arrContext), $totalLength));
  229.                     $objTemplate->context = preg_replace('((?<=^|\PL|\p{Hiragana}|\p{Katakana}|\p{Han}|\p{Myanmar}|\p{Khmer}|\p{Lao}|\p{Thai}|\p{Tibetan})(' . implode('|', array_map('preg_quote', $arrMatches)) . ')(?=\PL|\p{Hiragana}|\p{Katakana}|\p{Han}|\p{Myanmar}|\p{Khmer}|\p{Lao}|\p{Thai}|\p{Tibetan}|$))ui', '<mark class="highlight">$1</mark>', $objTemplate->context);
  230.                     $objTemplate->hasContext = true;
  231.                 }
  232.                 $this->addImageToTemplateFromSearchResult($arrResult[$i], $objTemplate);
  233.                 $this->Template->results .= $objTemplate->parse();
  234.             }
  235.             $this->Template->header = vsprintf($GLOBALS['TL_LANG']['MSC']['sResults'], array($from, $to, $count, $strKeywords));
  236.             $this->Template->duration = System::getFormattedNumber($query_endtime - $query_starttime, 3) . ' ' . $GLOBALS['TL_LANG']['MSC']['seconds'];
  237.         }
  238.     }
  239.     protected function addImageToTemplateFromSearchResult(array $result, Template $template): void
  240.     {
  241.         $template->hasImage = false;
  242.         if (!isset($result['meta']))
  243.         {
  244.             return;
  245.         }
  246.         $meta = json_decode($result['meta'], true);
  247.         foreach ($meta as $v)
  248.         {
  249.             if (!isset($v['https://schema.org/primaryImageOfPage']['contentUrl']))
  250.             {
  251.                 continue;
  252.             }
  253.             $baseUrls = array_filter(array(Environment::get('base'), System::getContainer()->get('contao.assets.files_context')->getStaticUrl()));
  254.             $figureBuilder = System::getContainer()->get('contao.image.studio')->createFigureBuilder();
  255.             $figureBuilder->fromUrl($v['https://schema.org/primaryImageOfPage']['contentUrl'], $baseUrls);
  256.             $figureMeta = new Metadata(array_filter(array(
  257.                 Metadata::VALUE_CAPTION => $v['https://schema.org/primaryImageOfPage']['caption'] ?? null,
  258.                 Metadata::VALUE_TITLE => $v['https://schema.org/primaryImageOfPage']['name'] ?? null,
  259.                 Metadata::VALUE_ALT => $v['https://schema.org/primaryImageOfPage']['alternateName'] ?? null,
  260.             )));
  261.             $figure = $figureBuilder
  262.                 ->setSize($this->imgSize)
  263.                 ->setMetadata($figureMeta)
  264.                 ->setLinkHref($result['url'])
  265.                 ->buildIfResourceExists();
  266.             if (null === $figure)
  267.             {
  268.                 continue;
  269.             }
  270.             $template->hasImage = true;
  271.             $template->figure = $figure;
  272.             $template->image = (object) $figure->getLegacyTemplateData();
  273.             return;
  274.         }
  275.     }
  276. }
  277. class_alias(ModuleSearch::class, 'ModuleSearch');