first commit

This commit is contained in:
Sven Wappler
2021-04-17 00:26:33 +02:00
commit 866c63cc63
813 changed files with 100696 additions and 0 deletions

View File

@@ -0,0 +1,217 @@
<?php
namespace WapplerSystems\Meilisearch;
/***************************************************************
* Copyright notice
*
* (c) 2015-2016 Timo Schmidt <timo.schmidt@dkd.de>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project is
* free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use WapplerSystems\Meilisearch\Domain\Index\Queue\RecordMonitor\Helper\ConfigurationAwareRecordService;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Database\QueryGenerator;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Changes in TYPO3 have an impact on the solr content and are caught
* by the GarbageCollector and RecordMonitor. Both act as a TCE Main Hook.
*
* This base class is used to share functionality that are needed for both
* to perform the changes in the data handler on the solr index.
*
* @author Timo Schmidt <timo.schmidt@dkd.de>
*/
abstract class AbstractDataHandlerListener
{
/**
* Reference to the configuration manager
*
* @var \WapplerSystems\Meilisearch\Domain\Index\Queue\RecordMonitor\Helper\ConfigurationAwareRecordService
*/
protected $configurationAwareRecordService;
/**
* @var FrontendEnvironment
*/
protected $frontendEnvironment = null;
/**
* AbstractDataHandlerListener constructor.
* @param ConfigurationAwareRecordService|null $recordService
*/
public function __construct(ConfigurationAwareRecordService $recordService = null, FrontendEnvironment $frontendEnvironment = null)
{
$this->configurationAwareRecordService = $recordService ?? GeneralUtility::makeInstance(ConfigurationAwareRecordService::class);
$this->frontendEnvironment = $frontendEnvironment ?? GeneralUtility::makeInstance(FrontendEnvironment::class);
}
/**
* @return array
*/
protected function getAllRelevantFieldsForCurrentState()
{
$allCurrentStateFieldnames = [];
foreach ($this->getUpdateSubPagesRecursiveTriggerConfiguration() as $triggerConfiguration) {
if (!isset($triggerConfiguration['currentState']) || !is_array($triggerConfiguration['currentState'])) {
// when no "currentState" configuration for the trigger exists we can skip it
continue;
}
// we collect the currentState fields to return a unique list of all fields
$allCurrentStateFieldnames = array_merge($allCurrentStateFieldnames, array_keys($triggerConfiguration['currentState']));
}
return array_unique($allCurrentStateFieldnames);
}
/**
* When the extend to subpages flag was set, we determine the affected subpages and return them.
*
* @param int $pageId
* @return array
*/
protected function getSubPageIds($pageId)
{
/** @var $queryGenerator \TYPO3\CMS\Core\Database\QueryGenerator */
$queryGenerator = GeneralUtility::makeInstance(QueryGenerator::class);
// here we retrieve only the subpages of this page because the permission clause is not evaluated
// on the root node.
$permissionClause = ' 1 ' . BackendUtility::BEenableFields('pages');
$treePageIdList = $queryGenerator->getTreeList($pageId, 20, 0, $permissionClause);
$treePageIds = array_map('intval', explode(',', $treePageIdList));
// the first one can be ignored because this is the page itself
array_shift($treePageIds);
return $treePageIds;
}
/**
* Checks if a page update will trigger a recursive update of pages
*
* This can either be the case if some $changedFields are part of the RecursiveUpdateTriggerConfiguration or
* columns have explicitly been configured via plugin.tx_meilisearch.index.queue.recursiveUpdateFields
*
* @param int $pageId
* @param array $changedFields
* @return bool
*/
protected function isRecursivePageUpdateRequired($pageId, $changedFields)
{
// First check RecursiveUpdateTriggerConfiguration
$isRecursiveUpdateRequired = $this->isRecursiveUpdateRequired($pageId, $changedFields);
// If RecursiveUpdateTriggerConfiguration is false => check if changeFields are part of recursiveUpdateFields
if ($isRecursiveUpdateRequired === false) {
$solrConfiguration = $this->frontendEnvironment->getSolrConfigurationFromPageId($pageId);
$indexQueueConfigurationName = $this->configurationAwareRecordService->getIndexingConfigurationName('pages', $pageId, $solrConfiguration);
if ($indexQueueConfigurationName === null) {
return false;
}
$updateFields = $solrConfiguration->getIndexQueueConfigurationRecursiveUpdateFields($indexQueueConfigurationName);
// Check if no additional fields have been defined and then skip recursive update
if (empty($updateFields)) {
return false;
}
// If the recursiveUpdateFields configuration is not part of the $changedFields skip recursive update
if (!array_intersect_key($changedFields, $updateFields)) {
return false;
}
}
return true;
}
/**
* @param int $pageId
* @param array $changedFields
* @return bool
*/
protected function isRecursiveUpdateRequired($pageId, $changedFields)
{
$fieldsForCurrentState = $this->getAllRelevantFieldsForCurrentState();
$fieldListToRetrieve = implode(',', $fieldsForCurrentState);
$page = BackendUtility::getRecord('pages', $pageId, $fieldListToRetrieve, '', false);
foreach ($this->getUpdateSubPagesRecursiveTriggerConfiguration() as $triggerConfiguration) {
$allCurrentStateFieldsMatch = $this->getAllCurrentStateFieldsMatch($triggerConfiguration, $page);
$allChangeSetValuesMatch = $this->getAllChangeSetValuesMatch($triggerConfiguration, $changedFields);
$aMatchingTriggerHasBeenFound = $allCurrentStateFieldsMatch && $allChangeSetValuesMatch;
if ($aMatchingTriggerHasBeenFound) {
return true;
}
}
return false;
}
/**
* @param array $triggerConfiguration
* @param array $pageRecord
* @return bool
*/
protected function getAllCurrentStateFieldsMatch($triggerConfiguration, $pageRecord)
{
$triggerConfigurationHasNoCurrentStateConfiguration = !array_key_exists('currentState', $triggerConfiguration);
if ($triggerConfigurationHasNoCurrentStateConfiguration) {
return true;
}
$diff = array_diff_assoc($triggerConfiguration['currentState'], $pageRecord);
return empty($diff);
}
/**
* @param array $triggerConfiguration
* @param array $changedFields
* @return bool
*/
protected function getAllChangeSetValuesMatch($triggerConfiguration, $changedFields)
{
$triggerConfigurationHasNoChangeSetStateConfiguration = !array_key_exists('changeSet', $triggerConfiguration);
if ($triggerConfigurationHasNoChangeSetStateConfiguration) {
return true;
}
$diff = array_diff_assoc($triggerConfiguration['changeSet'], $changedFields);
return empty($diff);
}
/**
* The implementation of this method need to retrieve a configuration to determine which record data
* and change combination required a recursive change.
*
* The structure needs to be:
*
* [
* [
* 'currentState' => ['fieldName1' => 'value1'],
* 'changeSet' => ['fieldName1' => 'value1']
* ]
* ]
*
* When the all values of the currentState AND all values of the changeSet match, a recursive update
* will be triggered.
*
* @return array
*/
abstract protected function getUpdateSubPagesRecursiveTriggerConfiguration();
}

228
Classes/Access/Rootline.php Normal file
View File

@@ -0,0 +1,228 @@
<?php
namespace WapplerSystems\Meilisearch\Access;
/***************************************************************
* Copyright notice
*
* (c) 2011-2015 Ingo Renner <ingo@typo3.org>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project is
* free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\RootlineUtility;
use TYPO3\CMS\Frontend\Page\PageRepository;
/**
* "Access Rootline", represents all pages and specifically those setting
* frontend user group access restrictions in a page's rootline.
*
* The access rootline only contains pages which set frontend user access
* restrictions and extend them to sub-pages. The format is as follows:
*
* pageId1:group1,group2/pageId2:group3/c:group1,group4,groupN
*
* The single elements of the access rootline are separated by a slash
* character. All but the last elements represent pages, the last element
* defines the access restrictions applied to the page's content elements
* and records shown on the page.
* Each page element is composed by the page ID of the page setting frontend
* user access restrictions, a colon, and a comma separated list of frontend
* user group IDs restricting access to the page.
* The content access element does not have a page ID, instead it replaces
* the ID by a lower case C.
*
* The groups for page elements are compared using OR, so the user needs to be
* a member of only one of the groups listed for a page. The elements are
* checked combined using AND, so the user must be member of at least one
* group in each page element. However, the groups in the content access
* element are checked using AND. So the user must be member of all the groups
* listed in the content access element to see the document.
*
* An access rootline for a generic record could instead be short like this:
*
* r:group1,group2,groupN
*
* In this case the lower case R tells us that we're dealing with a record
* like tt_news or the like. For records the groups are checked using OR
* instead of using AND as it would be the case with content elements.
*
* @author Ingo Renner <ingo@typo3.org>
*/
class Rootline
{
/**
* Delimiter for page and content access right elements in the rootline.
*
* @var string
*/
const ELEMENT_DELIMITER = '/';
/**
* Storage for access rootline elements
*
* @var array
*/
protected $rootlineElements = [];
/**
* Constructor, turns a string representation of an access rootline into an
* object representation.
*
* @param string $accessRootline Access Rootline String representation.
*/
public function __construct($accessRootline = null)
{
if (!is_null($accessRootline)) {
$rawRootlineElements = explode(self::ELEMENT_DELIMITER, $accessRootline);
foreach ($rawRootlineElements as $rawRootlineElement) {
try {
$this->push(GeneralUtility::makeInstance(RootlineElement::class, /** @scrutinizer ignore-type */ $rawRootlineElement));
} catch (RootlineElementFormatException $e) {
// just ignore the faulty element for now, might log this later
}
}
}
}
/**
* Adds an Access Rootline Element to the end of the rootline.
*
* @param RootlineElement $rootlineElement Element to add.
*/
public function push(RootlineElement $rootlineElement)
{
$lastElementIndex = max(0, (count($this->rootlineElements) - 1));
if (!empty($this->rootlineElements[$lastElementIndex])) {
if ($this->rootlineElements[$lastElementIndex]->getType() == RootlineElement::ELEMENT_TYPE_CONTENT) {
throw new RootlineElementFormatException(
'Can not add an element to an Access Rootline whose\' last element is a content type element.',
1294422132
);
}
if ($this->rootlineElements[$lastElementIndex]->getType() == RootlineElement::ELEMENT_TYPE_RECORD) {
throw new RootlineElementFormatException(
'Can not add an element to an Access Rootline whose\' last element is a record type element.',
1308343423
);
}
}
$this->rootlineElements[] = $rootlineElement;
}
/**
* Gets the Access Rootline for a specific page Id.
*
* @param int $pageId The page Id to generate the Access Rootline for.
* @param string $mountPointParameter The mount point parameter for generating the rootline.
* @return \WapplerSystems\Meilisearch\Access\Rootline Access Rootline for the given page Id.
*/
public static function getAccessRootlineByPageId(
$pageId,
$mountPointParameter = ''
) {
$accessRootline = GeneralUtility::makeInstance(Rootline::class);
$rootlineUtility = GeneralUtility::makeInstance(RootlineUtility::class, $pageId, $mountPointParameter);
try {
$rootline = $rootlineUtility->get();
} catch (\RuntimeException $e) {
$rootline = [];
}
$rootline = array_reverse($rootline);
// parent pages
foreach ($rootline as $pageRecord) {
if ($pageRecord['fe_group']
&& $pageRecord['extendToSubpages']
&& $pageRecord['uid'] != $pageId
) {
$accessRootline->push(GeneralUtility::makeInstance(
RootlineElement::class,
/** @scrutinizer ignore-type */ $pageRecord['uid'] . RootlineElement::PAGE_ID_GROUP_DELIMITER . $pageRecord['fe_group']
));
}
}
/** @var $pageSelector PageRepository */
$pageSelector = GeneralUtility::makeInstance(PageRepository::class);
// current page
$currentPageRecord = $pageSelector->getPage($pageId, true);
if ($currentPageRecord['fe_group']) {
$accessRootline->push(GeneralUtility::makeInstance(
RootlineElement::class,
/** @scrutinizer ignore-type */ $currentPageRecord['uid'] . RootlineElement::PAGE_ID_GROUP_DELIMITER . $currentPageRecord['fe_group']
));
}
return $accessRootline;
}
/**
* Returns the string representation of the access rootline.
*
* @return string String representation of the access rootline.
*/
public function __toString()
{
$stringElements = [];
foreach ($this->rootlineElements as $rootlineElement) {
$stringElements[] = (string)$rootlineElement;
}
return implode(self::ELEMENT_DELIMITER, $stringElements);
}
/**
* Gets a the groups in the Access Rootline.
*
* @return array An array of sorted, unique user group IDs required to access a page.
*/
public function getGroups()
{
$groups = [];
foreach ($this->rootlineElements as $rootlineElement) {
$rootlineElementGroups = $rootlineElement->getGroups();
$groups = array_merge($groups, $rootlineElementGroups);
}
$groups = $this->cleanGroupArray($groups);
return $groups;
}
/**
* Cleans an array of frontend user group IDs. Removes duplicates and sorts
* the array.
*
* @param array $groups An array of frontend user group IDs
* @return array An array of cleaned frontend user group IDs, unique, sorted.
*/
public static function cleanGroupArray(array $groups)
{
$groups = array_unique($groups); // removes duplicates
sort($groups, SORT_NUMERIC); // sort
return $groups;
}
}

View File

@@ -0,0 +1,187 @@
<?php
namespace WapplerSystems\Meilisearch\Access;
/***************************************************************
* Copyright notice
*
* (c) 2011-2015 Ingo Renner <ingo@typo3.org>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project is
* free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* An element in the "Access Rootline". Represents the frontend user group
* access restrictions for a page, a page's content, or a generic record.
*
* @author Ingo Renner <ingo@typo3.org>
*/
class RootlineElement
{
/**
* Page access rootline element.
*
* @var int
*/
const ELEMENT_TYPE_PAGE = 1;
/**
* Content access rootline element.
*
* @var int
*/
const ELEMENT_TYPE_CONTENT = 2;
/**
* Record access rootline element.
*
* @var int
*/
const ELEMENT_TYPE_RECORD = 3;
/**
* Delimiter between the page ID and the groups set for a page.
*
* @var string
*/
const PAGE_ID_GROUP_DELIMITER = ':';
/**
* Access type, either page (default) or content. Depending on the type,
* access is granted differently. For pages the user must meet at least one
* group requirement, for content all group requirements must be met.
*
* @var int
*/
protected $type = self::ELEMENT_TYPE_PAGE;
/**
* Page Id for the element. NULL for the content type.
*
* @var int
*/
protected $pageId = null;
/**
* Set of access groups assigned to the element.
*
* @var array
*/
protected $accessGroups = [];
/**
* Constructor for RootlineElement.
*
* @param string $element String representation of an element in the access rootline, usually of the form pageId:commaSeparatedPageAccessGroups
* @throws RootlineElementFormatException on wrong access format.
*/
public function __construct($element)
{
$elementAccess = explode(self::PAGE_ID_GROUP_DELIMITER, $element);
if (count($elementAccess) === 1 || $elementAccess[0] === 'c') {
// the content access groups part of the access rootline
$this->type = self::ELEMENT_TYPE_CONTENT;
if (count($elementAccess) === 1) {
$elementGroups = $elementAccess[0];
} else {
$elementGroups = $elementAccess[1];
}
} elseif ($elementAccess[0] === 'r') {
// record element type
if (count($elementAccess) !== 2) {
throw new RootlineElementFormatException(
'Wrong Access Rootline Element format for a record type element.',
1308342937
);
}
$this->type = self::ELEMENT_TYPE_RECORD;
$elementGroups = $elementAccess[1];
} else {
// page element type
if (count($elementAccess) !== 2 || !is_numeric($elementAccess[0])) {
throw new RootlineElementFormatException(
'Wrong Access Rootline Element format for a page type element.',
1294421105
);
}
$this->pageId = intval($elementAccess[0]);
$elementGroups = $elementAccess[1];
}
$this->accessGroups = GeneralUtility::intExplode(',', $elementGroups);
}
/**
* Returns the String representation of an access rootline element.
*
* @return string Access Rootline Element string representation
*/
public function __toString()
{
$rootlineElement = '';
if ($this->type == self::ELEMENT_TYPE_CONTENT) {
$rootlineElement .= 'c';
} elseif ($this->type == self::ELEMENT_TYPE_RECORD) {
$rootlineElement .= 'r';
} else {
$rootlineElement .= $this->pageId;
}
$rootlineElement .= self::PAGE_ID_GROUP_DELIMITER;
$rootlineElement .= implode(',', $this->accessGroups);
return $rootlineElement;
}
/**
* Gets the access rootline element's type.
*
* @return int ELEMENT_TYPE_PAGE for page, ELEMENT_TYPE_CONTENT for content access rootline elements
*/
public function getType()
{
return $this->type;
}
/**
* Gets the page Id for page type elements.
*
* @return int Page Id.
*/
public function getPageId()
{
return $this->pageId;
}
/**
* Gets the element's access group restrictions.
*
* @return array Array of user group Ids
*/
public function getGroups()
{
return $this->accessGroups;
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace WapplerSystems\Meilisearch\Access;
/***************************************************************
* Copyright notice
*
* (c) 2011-2015 Ingo Renner <ingo@typo3.org>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project is
* free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
/**
* Signals a wrong format for the access definition of a page or the content.
*
* @author Ingo Renner <ingo@typo3.org>
*/
class RootlineElementFormatException extends \InvalidArgumentException
{
}

View File

@@ -0,0 +1,126 @@
<?php
namespace WapplerSystems\Meilisearch;
/***************************************************************
* Copyright notice
*
* (c) 2011-2015 Ingo Renner <ingo@typo3.org>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project is
* free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use WapplerSystems\Meilisearch\System\Configuration\TypoScriptConfiguration;
use WapplerSystems\Meilisearch\System\ContentObject\ContentObjectService;
use WapplerSystems\Meilisearch\System\Solr\Document\Document;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Additional fields indexer.
*
* @todo Move this to an Index Queue frontend helper
*
* Adds page document fields as configured in
* plugin.tx_meilisearch.index.additionalFields.
*
* @author Ingo Renner <ingo@typo3.org>
*/
class AdditionalFieldsIndexer implements SubstitutePageIndexer
{
/**
* @var TypoScriptConfiguration
*/
protected $configuration;
/**
* @var array
*/
protected $additionalIndexingFields = [];
/**
* @var array
*/
protected $additionalFieldNames = [];
/**
* @var ContentObjectService
*/
protected $contentObjectService = null;
/**
* @param TypoScriptConfiguration $configuration
* @param ContentObjectService $contentObjectService
*/
public function __construct(TypoScriptConfiguration $configuration = null, ContentObjectService $contentObjectService = null)
{
$this->configuration = $configuration === null ? Util::getSolrConfiguration() : $configuration;
$this->additionalIndexingFields = $this->configuration->getIndexAdditionalFieldsConfiguration();
$this->additionalFieldNames = $this->configuration->getIndexMappedAdditionalFieldNames();
$this->contentObjectService = $contentObjectService === null ? GeneralUtility::makeInstance(ContentObjectService::class) : $contentObjectService;
}
/**
* Returns a substitute document for the currently being indexed page.
*
* Uses the original document and adds fields as defined in
* plugin.tx_meilisearch.index.additionalFields.
*
* @param Document $pageDocument The original page document.
* @return Document A Apache Solr Document object that replace the default page document
*/
public function getPageDocument(Document $pageDocument)
{
$substitutePageDocument = clone $pageDocument;
$additionalFields = $this->getAdditionalFields();
foreach ($additionalFields as $fieldName => $fieldValue) {
if (!isset($pageDocument->{$fieldName})) {
// making sure we only _add_ new fields
$substitutePageDocument->setField($fieldName, $fieldValue);
}
}
return $substitutePageDocument;
}
/**
* Gets the additional fields as an array mapping field names to values.
*
* @return array An array mapping additional field names to their values.
*/
protected function getAdditionalFields()
{
$additionalFields = [];
foreach ($this->additionalFieldNames as $additionalFieldName) {
$additionalFields[$additionalFieldName] = $this->getFieldValue($additionalFieldName);
}
return $additionalFields;
}
/**
* Uses the page's cObj instance to resolve the additional field's value.
*
* @param string $fieldName The name of the field to get.
* @return string The field's value.
*/
protected function getFieldValue($fieldName)
{
return $this->contentObjectService->renderSingleContentObjectByArrayAndKey($this->additionalIndexingFields, $fieldName);
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace WapplerSystems\Meilisearch;
/***************************************************************
* Copyright notice
*
* (c) 2012-2015 Ingo Renner <ingo@typo3.org>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project is
* free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
* A copy is found in the textfile GPL.txt and important notices to the license
* from the author is found in LICENSE.txt distributed with these scripts.
*
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use WapplerSystems\Meilisearch\System\Solr\Document\Document;
/**
* Interface that defines the method an indexer must implement to provide
* additional documents to index for a page being indexed.
*
* @author Ingo Renner <ingo@typo3.org>
*/
interface AdditionalPageIndexer
{
/**
* Provides additional documents that should be indexed together with a page.
*
* @param Document $pageDocument The original page document.
* @param array $allDocuments An array containing all the documents collected until here, including the page document
* @return array An array of additional \WapplerSystems\Meilisearch\System\Solr\Document\Document objects
*/
public function getAdditionalPageDocuments(Document $pageDocument, array $allDocuments);
}

58
Classes/Api.php Normal file
View File

@@ -0,0 +1,58 @@
<?php
namespace WapplerSystems\Meilisearch;
/***************************************************************
* Copyright notice
*
* (c) 2012-2015 Ingo Renner <ingo@typo3.org>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project is
* free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
/**
* Remote API related methods
*
* @author Ingo Renner <ingo@typo3.org>
*/
class Api
{
/**
* Checks whether a string is a valid API key.
*
* @param string $apiKey API key to check for validity
* @return bool TRUE if the API key is valid, FALSE otherwise
*/
public static function isValidApiKey($apiKey)
{
return ($apiKey === self::getApiKey());
}
/**
* Generates the API key for the REST API
*
* @return string API key for this installation
*/
public static function getApiKey()
{
return sha1(
$GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey'] .
'tx_meilisearch_api'
);
}
}

View File

@@ -0,0 +1,213 @@
<?php
namespace WapplerSystems\Meilisearch\Backend;
/***************************************************************
* Copyright notice
*
* (c) 2013-2015 Ingo Renner <ingo@typo3.org>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project is
* free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use WapplerSystems\Meilisearch\Domain\Site\Site;
use TYPO3\CMS\Backend\Form\FormResultCompiler;
use TYPO3\CMS\Backend\Form\NodeFactory;
use TYPO3\CMS\Core\Imaging\IconFactory;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Index Queue indexing configuration selector form field.
*
* @author Ingo Renner <ingo@typo3.org>
*/
class IndexingConfigurationSelectorField
{
/**
* Site used to determine indexing configurations
*
* @var Site
*/
protected $site;
/**
* Form element name
*
* @var string
*/
protected $formElementName = 'tx_meilisearch-index-queue-indexing-configuration-selector';
/**
* Selected values
*
* @var array
*/
protected $selectedValues = [];
/**
* Constructor
*
* @param Site $site The site to use to determine indexing configurations
*/
public function __construct(Site $site = null)
{
$this->site = $site;
}
/**
* Sets the form element name.
*
* @param string $formElementName Form element name
*/
public function setFormElementName($formElementName)
{
$this->formElementName = $formElementName;
}
/**
* Gets the form element name.
*
* @return string form element name
*/
public function getFormElementName()
{
return $this->formElementName;
}
/**
* Sets the selected values.
*
* @param array $selectedValues
*/
public function setSelectedValues(array $selectedValues)
{
$this->selectedValues = $selectedValues;
}
/**
* Gets the selected values.
*
* @return array
*/
public function getSelectedValues()
{
return $this->selectedValues;
}
/**
* Renders a field to select which indexing configurations to initialize.
*
* Uses \TYPO3\CMS\Backend\Form\FormEngine.
*
* @return string Markup for the select field
*/
public function render()
{
// transform selected values into the format used by TCEforms
$selectedValues = $this->selectedValues;
$tablesToIndex = $this->getIndexQueueConfigurationTableMap();
$formField = $this->renderSelectCheckbox($this->buildSelectorItems($tablesToIndex), $selectedValues);
// need to wrap the field in a TCEforms table to make the CSS apply
$form[] = '<div class="typo3-TCEforms tx_meilisearch-TCEforms">';
$form[] = $formField;
$form[] = '</div>';
return implode(LF, $form);
}
/**
* Builds a map of indexing configuration names to tables to to index.
*
* @return array Indexing configuration to database table map
*/
protected function getIndexQueueConfigurationTableMap()
{
$indexingTableMap = [];
$solrConfiguration = $this->site->getSolrConfiguration();
$configurationNames = $solrConfiguration->getEnabledIndexQueueConfigurationNames();
foreach ($configurationNames as $configurationName) {
$indexingTableMap[$configurationName] = $solrConfiguration->getIndexQueueTableNameOrFallbackToConfigurationName($configurationName);
}
return $indexingTableMap;
}
/**
* Builds the items to render in the TCEforms select field.
*
* @param array $tablesToIndex A map of indexing configuration to database tables
*
* @return array Selectable items for the TCEforms select field
*/
protected function buildSelectorItems(array $tablesToIndex)
{
$selectorItems = [];
$iconFactory = GeneralUtility::makeInstance(IconFactory::class);
foreach ($tablesToIndex as $configurationName => $tableName) {
$icon = $iconFactory->mapRecordTypeToIconIdentifier($tableName, []);
$labelTableName = '';
if ($configurationName !== $tableName) {
$labelTableName = ' (' . $tableName . ')';
}
$selectorItems[] = [$configurationName . $labelTableName, $configurationName, $icon];
}
return $selectorItems;
}
/**
* @param array $items
* @param string $selectedValues
*
* @return string
* @throws \TYPO3\CMS\Backend\Form\Exception
*/
protected function renderSelectCheckbox($items, $selectedValues)
{
$parameterArray = [
'fieldChangeFunc' => [],
'itemFormElName' => $this->formElementName,
'itemFormElValue' => $selectedValues,
'fieldConf' => ['config' => ['items' => $items]],
'fieldTSConfig' => ['noMatchingValue_label' => '']
];
$nodeFactory = GeneralUtility::makeInstance(NodeFactory::class);
$options = [
'renderType' => 'selectCheckBox', 'table' => 'tx_meilisearch_classes_backend_indexingconfigurationselector',
'fieldName' => 'additionalFields', 'databaseRow' => [], 'parameterArray' => $parameterArray
];
$options['parameterArray']['fieldConf']['config']['items'] = $items;
$options['parameterArray']['fieldTSConfig']['noMatchingValue_label'] = '';
$selectCheckboxResult = $nodeFactory->create($options)->render();
$formResultCompiler = GeneralUtility::makeInstance(FormResultCompiler::class);
$formResultCompiler->mergeResult($selectCheckboxResult);
$formHtml = isset($selectCheckboxResult['html']) ? $selectCheckboxResult['html'] : '';
$content = $formResultCompiler->addCssFiles() . $formHtml . $formResultCompiler->printNeededJSFunctions();
return $content;
}
}

View File

@@ -0,0 +1,72 @@
<?php
namespace WapplerSystems\Meilisearch\Backend;
/***************************************************************
* Copyright notice
*
* (c) 2017 - Thomas Hohn <tho@systime.dk>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project is
* free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use WapplerSystems\Meilisearch\Domain\Site\SiteRepository;
use WapplerSystems\Meilisearch\Domain\Site\Site;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* SiteSelectorField
*
* Responsible for generating SiteSelectorField
*
* @author Thomas Hohn <tho@systime.dk>
*/
class SiteSelectorField
{
/**
* Creates a dropdown selector of available TYPO3 sites with Solr configured.
*
* @param string $selectorName Name to be used in the select's name attribute
* @param Site $selectedSite Optional, currently selected site
* @return string Site selector HTML code
*/
public function getAvailableSitesSelector(
$selectorName,
Site $selectedSite = null
) {
$siteRepository = GeneralUtility::makeInstance(SiteRepository::class);
$sites = $siteRepository->getAvailableSites();
$selector = '<select name="' . htmlspecialchars($selectorName) . '" class="form-control">';
foreach ($sites as $site) {
$selectedAttribute = '';
if ($selectedSite !== null && $site->getRootPageId() === $selectedSite->getRootPageId()) {
$selectedAttribute = ' selected="selected"';
}
$selector .= '<option value="' . htmlspecialchars($site->getRootPageId()) . '"' . $selectedAttribute . '>'
. htmlspecialchars($site->getLabel())
. '</option>';
}
$selector .= '</select>';
return $selector;
}
}

View File

@@ -0,0 +1,267 @@
<?php
namespace WapplerSystems\Meilisearch;
/***************************************************************
* Copyright notice
*
* (c) 2010-2015 Ingo Renner <ingo@typo3.org>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project is
* free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use WapplerSystems\Meilisearch\Domain\Site\Site;
use WapplerSystems\Meilisearch\Domain\Site\SiteRepository;
use WapplerSystems\Meilisearch\System\Records\Pages\PagesRepository as PagesRepositoryAtExtSolr;
use WapplerSystems\Meilisearch\System\Records\SystemLanguage\SystemLanguageRepository;
use WapplerSystems\Meilisearch\System\Solr\Node;
use WapplerSystems\Meilisearch\System\Solr\SolrConnection;
use InvalidArgumentException;
use TYPO3\CMS\Core\Registry;
use TYPO3\CMS\Core\SingletonInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use function json_encode;
/**
* ConnectionManager is responsible to create SolrConnection objects.
*
* @author Ingo Renner <ingo@typo3.org>
*/
class ConnectionManager implements SingletonInterface
{
/**
* @var array
*/
protected static $connections = [];
/**
* @var SystemLanguageRepository
*/
protected $systemLanguageRepository;
/**
* @var PagesRepositoryAtExtSolr
*/
protected $pagesRepositoryAtExtSolr;
/**
* @var SiteRepository
*/
protected $siteRepository;
/**
* @param SystemLanguageRepository $systemLanguageRepository
* @param PagesRepositoryAtExtSolr|null $pagesRepositoryAtExtSolr
* @param SiteRepository $siteRepository
*/
public function __construct(
SystemLanguageRepository $systemLanguageRepository = null,
PagesRepositoryAtExtSolr $pagesRepositoryAtExtSolr = null,
SiteRepository $siteRepository = null
)
{
$this->systemLanguageRepository = $systemLanguageRepository ?? GeneralUtility::makeInstance(SystemLanguageRepository::class);
$this->siteRepository = $siteRepository ?? GeneralUtility::makeInstance(SiteRepository::class);
$this->pagesRepositoryAtExtSolr = $pagesRepositoryAtExtSolr ?? GeneralUtility::makeInstance(PagesRepositoryAtExtSolr::class);
}
/**
* Creates a solr connection for read and write endpoints
*
* @param array $readNodeConfiguration
* @param array $writeNodeConfiguration
* @return SolrConnection|object
*/
public function getSolrConnectionForNodes(array $readNodeConfiguration, array $writeNodeConfiguration)
{
$connectionHash = md5(json_encode($readNodeConfiguration) . json_encode($writeNodeConfiguration));
if (!isset(self::$connections[$connectionHash])) {
$readNode = Node::fromArray($readNodeConfiguration);
$writeNode = Node::fromArray($writeNodeConfiguration);
self::$connections[$connectionHash] = GeneralUtility::makeInstance(SolrConnection::class, $readNode, $writeNode);
}
return self::$connections[$connectionHash];
}
/**
* Creates a solr configuration from the configuration array and returns it.
*
* @param array $config The solr configuration array
* @return SolrConnection
*/
public function getConnectionFromConfiguration(array $config)
{
if(empty($config['read']) && !empty($config['solrHost'])) {
throw new InvalidArgumentException('Invalid registry data please re-initialize your solr connections');
}
return $this->getSolrConnectionForNodes($config['read'], $config['write']);
}
/**
* Gets a Solr connection for a page ID.
*
* @param int $pageId A page ID.
* @param int $language The language ID to get the connection for as the path may differ. Optional, defaults to 0.
* @param string $mount Comma list of MountPoint parameters
* @return SolrConnection A solr connection.
* @throws NoSolrConnectionFoundException
*/
public function getConnectionByPageId($pageId, $language = 0, $mount = '')
{
try {
$site = $this->siteRepository->getSiteByPageId($pageId, $mount);
$this->throwExceptionOnInvalidSite($site, 'No site for pageId ' . $pageId);
$config = $site->getSolrConnectionConfiguration($language);
$solrConnection = $this->getConnectionFromConfiguration($config);
return $solrConnection;
} catch(InvalidArgumentException $e) {
$noSolrConnectionException = $this->buildNoConnectionExceptionForPageAndLanguage($pageId, $language);
throw $noSolrConnectionException;
}
}
/**
* Gets a Solr connection for a root page ID.
*
* @param int $pageId A root page ID.
* @param int $language The language ID to get the connection for as the path may differ. Optional, defaults to 0.
* @return SolrConnection A solr connection.
* @throws NoSolrConnectionFoundException
*/
public function getConnectionByRootPageId($pageId, $language = 0)
{
try {
$site = $this->siteRepository->getSiteByRootPageId($pageId);
$this->throwExceptionOnInvalidSite($site, 'No site for pageId ' . $pageId);
$config = $site->getSolrConnectionConfiguration($language);
$solrConnection = $this->getConnectionFromConfiguration($config);
return $solrConnection;
} catch (InvalidArgumentException $e) {
/* @var NoSolrConnectionFoundException $noSolrConnectionException */
$noSolrConnectionException = $this->buildNoConnectionExceptionForPageAndLanguage($pageId, $language);
throw $noSolrConnectionException;
}
}
/**
* Gets all connections found.
*
* @return SolrConnection[] An array of initialized WapplerSystems\Meilisearch\System\Solr\SolrConnection connections
* @throws NoSolrConnectionFoundException
*/
public function getAllConnections()
{
$solrConnections = [];
foreach ($this->siteRepository->getAvailableSites() as $site) {
foreach ($site->getAllSolrConnectionConfigurations() as $solrConfiguration) {
$solrConnections[] = $this->getConnectionFromConfiguration($solrConfiguration);
}
}
return $solrConnections;
}
/**
* Gets all connections configured for a given site.
*
* @param Site $site A TYPO3 site
* @return SolrConnection[] An array of Solr connection objects (WapplerSystems\Meilisearch\System\Solr\SolrConnection)
* @throws NoSolrConnectionFoundException
*/
public function getConnectionsBySite(Site $site)
{
$connections = [];
foreach ($site->getAllSolrConnectionConfigurations() as $languageId => $solrConnectionConfiguration) {
$connections[$languageId] = $this->getConnectionFromConfiguration($solrConnectionConfiguration);
}
return $connections;
}
/**
* Creates a human readable label from the connections' configuration.
*
* @param array $connection Connection configuration
* @return string Connection label
*/
protected function buildConnectionLabel(array $connection)
{
return $connection['rootPageTitle']
. ' (pid: ' . $connection['rootPageUid']
. ', language: ' . $this->systemLanguageRepository->findOneLanguageTitleByLanguageId($connection['language'])
. ') - Read node: '
. $connection['read']['host'] . ':'
. $connection['read']['port']
. $connection['read']['path']
.' - Write node: '
. $connection['write']['host'] . ':'
. $connection['write']['port']
. $connection['write']['path'];
}
/**
* @param $pageId
* @param $language
* @return NoSolrConnectionFoundException
*/
protected function buildNoConnectionExceptionForPageAndLanguage($pageId, $language): NoSolrConnectionFoundException
{
$message = 'Could not find a Solr connection for page [' . $pageId . '] and language [' . $language . '].';
$noSolrConnectionException = $this->buildNoConnectionException($message);
$noSolrConnectionException->setLanguageId($language);
return $noSolrConnectionException;
}
/**
* Throws a no connection exception when no site was passed.
*
* @param Site|null $site
* @param $message
* @throws NoSolrConnectionFoundException
*/
protected function throwExceptionOnInvalidSite(?Site $site, string $message)
{
if (!is_null($site)) {
return;
}
throw $this->buildNoConnectionException($message);
}
/**
* Build a NoSolrConnectionFoundException with the passed message.
* @param string $message
* @return NoSolrConnectionFoundException
*/
protected function buildNoConnectionException(string $message): NoSolrConnectionFoundException
{
/* @var NoSolrConnectionFoundException $noSolrConnectionException */
$noSolrConnectionException = GeneralUtility::makeInstance(
NoSolrConnectionFoundException::class,
/** @scrutinizer ignore-type */
$message,
/** @scrutinizer ignore-type */
1575396474
);
return $noSolrConnectionException;
}
}

View File

@@ -0,0 +1,117 @@
<?php
namespace WapplerSystems\Meilisearch\ContentObject;
/***************************************************************
* Copyright notice
*
* (c) 2017 Timo Hund <timo.hund@dkd.de>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project is
* free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use WapplerSystems\Meilisearch\Domain\Index\Classification\Classification as ClassificationItem;
use WapplerSystems\Meilisearch\Domain\Index\Classification\ClassificationService;
use InvalidArgumentException;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\ContentObject\AbstractContentObject;
/**
* A content object (cObj) to classify content based on a configuration.
*
* Example usage:
*
* keywords = SOLR_CLASSIFICATION # supports stdWrap
* keywords {
* field = __solr_content # a comma separated field. instead of field you can also use "value"
* classes {
* 1 {
* patterns = smartphone, mobile, mobilephone # list of patterns that need to match to assign that class
* class = mobilephone # class that should be assigned when a pattern matches
* }
* }
* }
*/
class Classification extends AbstractContentObject
{
const CONTENT_OBJECT_NAME = 'SOLR_CLASSIFICATION';
/**
* Executes the SOLR_CLASSIFICATION content object.
*
* Returns mapped classes when the field matches on of the configured patterns ...
*
* @inheritDoc
*/
public function render($conf = [])
{
if (!is_array($conf['classes.'])) {
throw new InvalidArgumentException('No class configuration configured for SOLR_CLASSIFICATION object. Given configuration: ' . serialize($conf));
}
$configuredMappedClasses = $conf['classes.'];
unset($conf['classes.']);
$data = '';
if (isset($conf['value'])) {
$data = $conf['value'];
unset($conf['value']);
}
if (!empty($conf)) {
$data = $this->cObj->stdWrap($data, $conf);
}
$classifications = $this->buildClassificationsFromConfiguration($configuredMappedClasses);
/** @var $classificationService ClassificationService */
$classificationService = GeneralUtility::makeInstance(ClassificationService::class);
return serialize($classificationService->getMatchingClassNames((string)$data, $classifications));
}
/**
* Builds an array of Classification objects from the passed classification configuration.
*
* @param array $configuredMappedClasses
* @return ClassificationItem[]
*/
protected function buildClassificationsFromConfiguration($configuredMappedClasses) : array
{
$classifications = [];
foreach ($configuredMappedClasses as $class) {
if ( (empty($class['patterns']) && empty($class['matchPatterns'])) || empty($class['class'])) {
throw new InvalidArgumentException('A class configuration in SOLR_CLASSIFCATION needs to have a pattern and a class configured. Given configuration: ' . serialize($class));
}
// @todo deprecate patterns configuration
$patterns = empty($class['patterns']) ? [] : GeneralUtility::trimExplode(',', $class['patterns']);
$matchPatterns = empty($class['matchPatterns']) ? [] : GeneralUtility::trimExplode(',', $class['matchPatterns']);
$matchPatterns = $matchPatterns + $patterns;
$unMatchPatters = empty($class['unmatchPatterns']) ? [] : GeneralUtility::trimExplode(',', $class['unmatchPatterns']);
$className = $class['class'];
$classifications[] = GeneralUtility::makeInstance(
ClassificationItem::class,
/** @scrutinizer ignore-type */ $matchPatterns,
/** @scrutinizer ignore-type */ $unMatchPatters,
/** @scrutinizer ignore-type */ $className
);
}
return $classifications;
}
}

View File

@@ -0,0 +1,80 @@
<?php
namespace WapplerSystems\Meilisearch\ContentObject;
/***************************************************************
* Copyright notice
*
* (c) 2011-2015 Ingo Renner <ingo@typo3.org>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project is
* free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use WapplerSystems\Meilisearch\HtmlContentExtractor;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\ContentObject\AbstractContentObject;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
/**
* A content object (cObj) to clean a database field in a way so that it can be
* used to fill a Solr document's content field.
*
* @author Ingo Renner <ingo@typo3.org>
*/
class Content extends AbstractContentObject
{
const CONTENT_OBJECT_NAME = 'SOLR_CONTENT';
/**
* Executes the SOLR_CONTENT content object.
*
* Cleans content coming from a database field, removing HTML tags ...
*
* @inheritDoc
*/
public function render($conf = [])
{
$contentExtractor = GeneralUtility::makeInstance(
HtmlContentExtractor::class,
/** @scrutinizer ignore-type */ $this->getRawContent($this->cObj, $conf)
);
return $contentExtractor->getIndexableContent();
}
/**
* Gets the raw content as configured - a certain value or database field.
*
* @param ContentObjectRenderer $contentObject The original content object
* @param array $configuration content object configuration
* @return string The raw content
*/
protected function getRawContent($contentObject, $configuration)
{
$content = '';
if (isset($configuration['value'])) {
$content = $configuration['value'];
unset($configuration['value']);
}
if (!empty($configuration)) {
$content = $contentObject->stdWrap($content, $configuration);
}
return $content;
}
}

View File

@@ -0,0 +1,92 @@
<?php
namespace WapplerSystems\Meilisearch\ContentObject;
/***************************************************************
* Copyright notice
*
* (c) 2011-2015 Ingo Renner <ingo@typo3.org>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project is
* free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\ContentObject\AbstractContentObject;
/**
* A content object (cObj) to turn comma separated strings into an array to be
* used in a multi value field in a Solr document.
*
* Example usage:
*
* keywords = SOLR_MULTIVALUE # supports stdWrap
* keywords {
* field = tags # a comma separated field. instead of field you can also use "value"
* separator = , # comma is the default value
* removeEmptyValues = 1 # a flag to remove empty strings from the list, on by default.
* removeDuplicateValues = 1 # a flag to remove duplicate strings from the list, off by default.
* }
*
* @author Ingo Renner <ingo@typo3.org>
*/
class Multivalue extends AbstractContentObject
{
const CONTENT_OBJECT_NAME = 'SOLR_MULTIVALUE';
/**
* Executes the SOLR_MULTIVALUE content object.
*
* Turns a list of values into an array that can then be used to fill
* multivalued fields in a Solr document. The array is returned in
* serialized form as content objects are expected to return strings.
*
* @inheritDoc
*/
public function render($conf = [])
{
$data = '';
if (isset($conf['value'])) {
$data = $conf['value'];
unset($conf['value']);
}
if (!empty($conf)) {
$data = $this->cObj->stdWrap($data, $conf);
}
if (!array_key_exists('separator', $conf)) {
$conf['separator'] = ',';
}
$removeEmptyValues = true;
if (isset($conf['removeEmptyValues']) && $conf['removeEmptyValues'] == 0) {
$removeEmptyValues = false;
}
$listAsArray = GeneralUtility::trimExplode(
$conf['separator'],
$data,
$removeEmptyValues
);
if (!empty($conf['removeDuplicateValues'])) {
$listAsArray = array_unique($listAsArray);
}
return serialize($listAsArray);
}
}

View File

@@ -0,0 +1,385 @@
<?php
namespace WapplerSystems\Meilisearch\ContentObject;
/***************************************************************
* Copyright notice
*
* (c) 2011-2015 Ingo Renner <ingo@typo3.org>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project is
* free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use WapplerSystems\Meilisearch\System\Language\FrontendOverlayService;
use WapplerSystems\Meilisearch\System\TCA\TCAService;
use WapplerSystems\Meilisearch\Util;
use Doctrine\DBAL\Driver\Statement;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\QueryBuilder;
use TYPO3\CMS\Core\Database\RelationHandler;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\ContentObject\AbstractContentObject;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
/**
* A content object (cObj) to resolve relations between database records
*
* Configuration options:
*
* localField: the record's field to use to resolve relations
* foreignLabelField: Usually the label field to retrieve from the related records is determined automatically using TCA, using this option the desired field can be specified explicitly
* multiValue: whether to return related records suitable for a multi value field
* singleValueGlue: when not using multiValue, the related records need to be concatenated using a glue string, by default this is ", ". Using this option a custom glue can be specified. The custom value must be wrapped by pipe (|) characters.
* relationTableSortingField: field in an mm relation table to sort by, usually "sorting"
* enableRecursiveValueResolution: if the specified remote table's label field is a relation to another table, the value will be resolve by following the relation recursively.
* removeEmptyValues: Removes empty values when resolving relations, defaults to TRUE
* removeDuplicateValues: Removes duplicate values
*
* @author Ingo Renner <ingo@typo3.org>
*/
class Relation extends AbstractContentObject
{
const CONTENT_OBJECT_NAME = 'SOLR_RELATION';
/**
* Content object configuration
*
* @var array
*/
protected $configuration = [];
/**
* @var TCAService
*/
protected $tcaService = null;
/**
* @var FrontendOverlayService
*/
protected $frontendOverlayService = null;
/**
* Relation constructor.
* @param TCAService|null $tcaService
* @param FrontendOverlayService|null $frontendOverlayService
*/
public function __construct(ContentObjectRenderer $cObj, TCAService $tcaService = null, FrontendOverlayService $frontendOverlayService = null)
{
$this->cObj = $cObj;
$this->configuration['enableRecursiveValueResolution'] = 1;
$this->configuration['removeEmptyValues'] = 1;
$this->tcaService = $tcaService ?? GeneralUtility::makeInstance(TCAService::class);
$this->frontendOverlayService = $frontendOverlayService ?? GeneralUtility::makeInstance(FrontendOverlayService::class);
}
/**
* Executes the SOLR_RELATION content object.
*
* Resolves relations between records. Currently supported relations are
* TYPO3-style m:n relations.
* May resolve single value and multi value relations.
*
* @inheritDoc
*/
public function render($conf = [])
{
$this->configuration = array_merge($this->configuration, $conf);
$relatedItems = $this->getRelatedItems($this->cObj);
if (!empty($this->configuration['removeDuplicateValues'])) {
$relatedItems = array_unique($relatedItems);
}
if (empty($conf['multiValue'])) {
// single value, need to concatenate related items
$singleValueGlue = !empty($conf['singleValueGlue']) ? trim($conf['singleValueGlue'], '|') : ', ';
$result = implode($singleValueGlue, $relatedItems);
} else {
// multi value, need to serialize as content objects must return strings
$result = serialize($relatedItems);
}
return $result;
}
/**
* Gets the related items of the current record's configured field.
*
* @param ContentObjectRenderer $parentContentObject parent content object
* @return array Array of related items, values already resolved from related records
*/
protected function getRelatedItems(ContentObjectRenderer $parentContentObject)
{
list($table, $uid) = explode(':', $parentContentObject->currentRecord);
$uid = (int) $uid;
$field = $this->configuration['localField'];
if (!$this->tcaService->getHasConfigurationForField($table, $field)) {
return [];
}
$overlayUid = $this->frontendOverlayService->getUidOfOverlay($table, $field, $uid);
$fieldTCA = $this->tcaService->getConfigurationForField($table, $field);
if (isset($fieldTCA['config']['MM']) && trim($fieldTCA['config']['MM']) !== '') {
$relatedItems = $this->getRelatedItemsFromMMTable($table, $overlayUid, $fieldTCA);
} else {
$relatedItems = $this->getRelatedItemsFromForeignTable($table, $overlayUid, $fieldTCA, $parentContentObject);
}
return $relatedItems;
}
/**
* Gets the related items from a table using a n:m relation.
*
* @param string $localTableName Local table name
* @param int $localRecordUid Local record uid
* @param array $localFieldTca The local table's TCA
* @return array Array of related items, values already resolved from related records
*/
protected function getRelatedItemsFromMMTable($localTableName, $localRecordUid, array $localFieldTca)
{
$relatedItems = [];
$foreignTableName = $localFieldTca['config']['foreign_table'];
$foreignTableTca = $this->tcaService->getTableConfiguration($foreignTableName);
$foreignTableLabelField = $this->resolveForeignTableLabelField($foreignTableTca);
$mmTableName = $localFieldTca['config']['MM'];
// Remove the first option of foreignLabelField for recursion
if (strpos($this->configuration['foreignLabelField'], '.') !== false) {
$foreignTableLabelFieldArr = explode('.', $this->configuration['foreignLabelField']);
unset($foreignTableLabelFieldArr[0]);
$this->configuration['foreignLabelField'] = implode('.', $foreignTableLabelFieldArr);
}
$relationHandler = GeneralUtility::makeInstance(RelationHandler::class);
$relationHandler->start('', $foreignTableName, $mmTableName, $localRecordUid, $localTableName, $localFieldTca['config']);
$selectUids = $relationHandler->tableArray[$foreignTableName];
if (!is_array($selectUids) || count($selectUids) <= 0) {
return $relatedItems;
}
$relatedRecords = $this->getRelatedRecords($foreignTableName, ...$selectUids);
foreach ($relatedRecords as $record) {
if (isset($foreignTableTca['columns'][$foreignTableLabelField]['config']['foreign_table'])
&& $this->configuration['enableRecursiveValueResolution']
) {
if (strpos($this->configuration['foreignLabelField'], '.') !== false) {
$foreignTableLabelFieldArr = explode('.', $this->configuration['foreignLabelField']);
unset($foreignTableLabelFieldArr[0]);
$this->configuration['foreignLabelField'] = implode('.', $foreignTableLabelFieldArr);
}
$this->configuration['localField'] = $foreignTableLabelField;
$contentObject = GeneralUtility::makeInstance(ContentObjectRenderer::class);
$contentObject->start($record, $foreignTableName);
return $this->getRelatedItems($contentObject);
} else {
if (Util::getLanguageUid() > 0) {
$record = $this->frontendOverlayService->getOverlay($foreignTableName, $record);
}
$relatedItems[] = $record[$foreignTableLabelField];
}
}
return $relatedItems;
}
/**
* Resolves the field to use as the related item's label depending on TCA
* and TypoScript configuration
*
* @param array $foreignTableTca The foreign table's TCA
* @return string The field to use for the related item's label
*/
protected function resolveForeignTableLabelField(array $foreignTableTca)
{
$foreignTableLabelField = $foreignTableTca['ctrl']['label'];
// when foreignLabelField is not enabled we can return directly
if (empty($this->configuration['foreignLabelField'])) {
return $foreignTableLabelField;
}
if (strpos($this->configuration['foreignLabelField'], '.') !== false) {
list($foreignTableLabelField) = explode('.', $this->configuration['foreignLabelField'], 2);
} else {
$foreignTableLabelField = $this->configuration['foreignLabelField'];
}
return $foreignTableLabelField;
}
/**
* Gets the related items from a table using a 1:n relation.
*
* @param string $localTableName Local table name
* @param int $localRecordUid Local record uid
* @param array $localFieldTca The local table's TCA
* @param ContentObjectRenderer $parentContentObject parent content object
* @return array Array of related items, values already resolved from related records
*/
protected function getRelatedItemsFromForeignTable(
$localTableName,
$localRecordUid,
array $localFieldTca,
ContentObjectRenderer $parentContentObject
) {
$relatedItems = [];
$foreignTableName = $localFieldTca['config']['foreign_table'];
$foreignTableTca = $this->tcaService->getTableConfiguration($foreignTableName);
$foreignTableLabelField = $this->resolveForeignTableLabelField($foreignTableTca);
/** @var $relationHandler RelationHandler */
$relationHandler = GeneralUtility::makeInstance(RelationHandler::class);
$itemList = $parentContentObject->data[$this->configuration['localField']] ?? '';
$relationHandler->start($itemList, $foreignTableName, '', $localRecordUid, $localTableName, $localFieldTca['config']);
$selectUids = $relationHandler->tableArray[$foreignTableName];
if (!is_array($selectUids) || count($selectUids) <= 0) {
return $relatedItems;
}
$relatedRecords = $this->getRelatedRecords($foreignTableName, ...$selectUids);
foreach ($relatedRecords as $relatedRecord) {
$resolveRelatedValue = $this->resolveRelatedValue(
$relatedRecord,
$foreignTableTca,
$foreignTableLabelField,
$parentContentObject,
$foreignTableName
);
if (!empty($resolveRelatedValue) || !$this->configuration['removeEmptyValues']) {
$relatedItems[] = $resolveRelatedValue;
}
}
return $relatedItems;
}
/**
* Resolves the value of the related field. If the related field's value is
* a relation itself, this method takes care of resolving it recursively.
*
* @param array $relatedRecord Related record as array
* @param array $foreignTableTca TCA of the related table
* @param string $foreignTableLabelField Field name of the foreign label field
* @param ContentObjectRenderer $parentContentObject cObject
* @param string $foreignTableName Related record table name
*
* @return string
*/
protected function resolveRelatedValue(
array $relatedRecord,
$foreignTableTca,
$foreignTableLabelField,
ContentObjectRenderer $parentContentObject,
$foreignTableName = ''
) {
if (Util::getLanguageUid() > 0 && !empty($foreignTableName)) {
$relatedRecord = $this->frontendOverlayService->getOverlay($foreignTableName, $relatedRecord);
}
$value = $relatedRecord[$foreignTableLabelField];
if (
!empty($foreignTableName)
&& isset($foreignTableTca['columns'][$foreignTableLabelField]['config']['foreign_table'])
&& $this->configuration['enableRecursiveValueResolution']
) {
// backup
$backupRecord = $parentContentObject->data;
$backupConfiguration = $this->configuration;
// adjust configuration for next level
$this->configuration['localField'] = $foreignTableLabelField;
$parentContentObject->data = $relatedRecord;
if (strpos($this->configuration['foreignLabelField'], '.') !== false) {
list(, $this->configuration['foreignLabelField']) = explode('.',
$this->configuration['foreignLabelField'], 2);
} else {
$this->configuration['foreignLabelField'] = '';
}
// recursion
$relatedItemsFromForeignTable = $this->getRelatedItemsFromForeignTable(
$foreignTableName,
$relatedRecord['uid'],
$foreignTableTca['columns'][$foreignTableLabelField],
$parentContentObject
);
$value = array_pop($relatedItemsFromForeignTable);
// restore
$this->configuration = $backupConfiguration;
$parentContentObject->data = $backupRecord;
}
return $parentContentObject->stdWrap($value, $this->configuration);
}
/**
* Return records via relation.
*
* @param string $foreignTable The table to fetch records from.
* @param int[] ...$uids The uids to fetch from table.
* @return array
*/
protected function getRelatedRecords($foreignTable, int ...$uids): array
{
/** @var QueryBuilder $queryBuilder */
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($foreignTable);
$queryBuilder->select('*')
->from($foreignTable)
->where($queryBuilder->expr()->in('uid', $uids));
if (isset($this->configuration['additionalWhereClause'])) {
$queryBuilder->andWhere($this->configuration['additionalWhereClause']);
}
$statement = $queryBuilder->execute();
return $this->sortByKeyInIN($statement, 'uid', ...$uids);
}
/**
* Sorts the result set by key in array for IN values.
* Simulates MySqls ORDER BY FIELD(fieldname, COPY_OF_IN_FOR_WHERE)
* Example: SELECT * FROM a_table WHERE field_name IN (2, 3, 4) SORT BY FIELD(field_name, 2, 3, 4)
*
*
* @param Statement $statement
* @param string $columnName
* @param array $arrayWithValuesForIN
* @return array
*/
protected function sortByKeyInIN(Statement $statement, string $columnName, ...$arrayWithValuesForIN) : array
{
$records = [];
while ($record = $statement->fetch()) {
$indexNumber = array_search($record[$columnName], $arrayWithValuesForIN);
$records[$indexNumber] = $record;
}
ksort($records);
return $records;
}
}

View File

@@ -0,0 +1,276 @@
<?php
namespace WapplerSystems\Meilisearch\Controller;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use WapplerSystems\Meilisearch\ConnectionManager;
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\SearchResultSetService;
use WapplerSystems\Meilisearch\Domain\Search\SearchRequestBuilder;
use WapplerSystems\Meilisearch\NoSolrConnectionFoundException;
use WapplerSystems\Meilisearch\Search;
use WapplerSystems\Meilisearch\System\Configuration\TypoScriptConfiguration;
use WapplerSystems\Meilisearch\Mvc\Controller\SolrControllerContext;
use WapplerSystems\Meilisearch\System\Logging\SolrLogManager;
use WapplerSystems\Meilisearch\System\Service\ConfigurationService;
use WapplerSystems\Meilisearch\System\Configuration\ConfigurationManager as SolrConfigurationManager;
use WapplerSystems\Meilisearch\Util;
use TYPO3\CMS\Core\TypoScript\TypoScriptService;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface;
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
/**
* Class AbstractBaseController
*
* @author Frans Saris <frans@beech.it>
* @author Timo Hund <timo.hund@dkd.de>
*/
abstract class AbstractBaseController extends ActionController
{
/**
* @var ContentObjectRenderer
*/
private $contentObjectRenderer;
/**
* @var TypoScriptFrontendController
*/
protected $typoScriptFrontendController;
/**
* @var \TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface
*/
protected $configurationManager;
/**
* @var SolrConfigurationManager
*/
private $solrConfigurationManager;
/**
* The configuration is private if you need it please get it from the controllerContext.
*
* @var TypoScriptConfiguration
*/
protected $typoScriptConfiguration;
/**
* @var \WapplerSystems\Meilisearch\Mvc\Controller\SolrControllerContext
*/
protected $controllerContext;
/**
* @var \WapplerSystems\Meilisearch\Domain\Search\ResultSet\SearchResultSetService
*/
protected $searchService;
/**
* @var \WapplerSystems\Meilisearch\Domain\Search\SearchRequestBuilder
*/
protected $searchRequestBuilder;
/**
* @var bool
*/
protected $resetConfigurationBeforeInitialize = true;
/**
* @param \TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface $configurationManager
* @return void
*/
public function injectConfigurationManager(ConfigurationManagerInterface $configurationManager)
{
$this->configurationManager = $configurationManager;
// @extensionScannerIgnoreLine
$this->contentObjectRenderer = $this->configurationManager->getContentObject();
}
/**
* @param \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer $contentObjectRenderer
*/
public function setContentObjectRenderer($contentObjectRenderer)
{
$this->contentObjectRenderer = $contentObjectRenderer;
}
/**
* @return \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer
*/
public function getContentObjectRenderer()
{
return $this->contentObjectRenderer;
}
/**
* @param SolrConfigurationManager $configurationManager
*/
public function injectSolrConfigurationManager(SolrConfigurationManager $configurationManager)
{
$this->solrConfigurationManager = $configurationManager;
}
/**
* @param boolean $resetConfigurationBeforeInitialize
*/
public function setResetConfigurationBeforeInitialize($resetConfigurationBeforeInitialize)
{
$this->resetConfigurationBeforeInitialize = $resetConfigurationBeforeInitialize;
}
/**
* Initialize the controller context
*
* @return \TYPO3\CMS\Extbase\Mvc\Controller\ControllerContext ControllerContext to be passed to the view
* @api
*/
protected function buildControllerContext()
{
/** @var $controllerContext \WapplerSystems\Meilisearch\Mvc\Controller\SolrControllerContext */
$controllerContext = $this->objectManager->get(SolrControllerContext::class);
$controllerContext->setRequest($this->request);
$controllerContext->setResponse($this->response);
if ($this->arguments !== null) {
$controllerContext->setArguments($this->arguments);
}
$controllerContext->setUriBuilder($this->uriBuilder);
$controllerContext->setTypoScriptConfiguration($this->typoScriptConfiguration);
return $controllerContext;
}
/**
* Initialize action
*/
protected function initializeAction()
{
// Reset configuration (to reset flexform overrides) if resetting is enabled
if ($this->resetConfigurationBeforeInitialize) {
$this->solrConfigurationManager->reset();
}
/** @var TypoScriptService $typoScriptService */
$typoScriptService = $this->objectManager->get(TypoScriptService::class);
// Merge settings done by typoscript with solrConfiguration plugin.tx_meilisearch (obsolete when part of ext:solr)
$frameWorkConfiguration = $this->configurationManager->getConfiguration(ConfigurationManagerInterface::CONFIGURATION_TYPE_FRAMEWORK);
$pluginSettings = [];
foreach (['search', 'settings', 'suggest', 'statistics', 'logging', 'general', 'solr', 'view'] as $key) {
if (isset($frameWorkConfiguration[$key])) {
$pluginSettings[$key] = $frameWorkConfiguration[$key];
}
}
$this->typoScriptConfiguration = $this->solrConfigurationManager->getTypoScriptConfiguration();
if ($pluginSettings !== []) {
$this->typoScriptConfiguration->mergeSolrConfiguration(
$typoScriptService->convertPlainArrayToTypoScriptArray($pluginSettings),
true,
false
);
}
$this->objectManager->get(ConfigurationService::class)
->overrideConfigurationWithFlexFormSettings(
$this->contentObjectRenderer->data['pi_flexform'],
$this->typoScriptConfiguration
);
parent::initializeAction();
$this->typoScriptFrontendController = $GLOBALS['TSFE'];
$this->initializeSettings();
if ($this->actionMethodName !== 'solrNotAvailableAction') {
$this->initializeSearch();
}
}
/**
* Inject settings of plugin.tx_meilisearch
*
* @return void
*/
protected function initializeSettings()
{
/** @var $typoScriptService TypoScriptService */
$typoScriptService = $this->objectManager->get(TypoScriptService::class);
// Make sure plugin.tx_meilisearch.settings are available in the view as {settings}
$this->settings = $typoScriptService->convertTypoScriptArrayToPlainArray(
$this->typoScriptConfiguration->getObjectByPathOrDefault('plugin.tx_meilisearch.settings.', [])
);
}
/**
* Initialize the Solr connection and
* test the connection through a ping
*/
protected function initializeSearch()
{
/** @var \WapplerSystems\Meilisearch\ConnectionManager $solrConnection */
try {
$solrConnection = $this->objectManager->get(ConnectionManager::class)->getConnectionByPageId($this->typoScriptFrontendController->id, Util::getLanguageUid(), $this->typoScriptFrontendController->MP);
$search = $this->objectManager->get(Search::class, $solrConnection);
$this->searchService = $this->objectManager->get(
SearchResultSetService::class,
/** @scrutinizer ignore-type */ $this->typoScriptConfiguration,
/** @scrutinizer ignore-type */ $search
);
} catch (NoSolrConnectionFoundException $e) {
$this->handleSolrUnavailable();
}
}
/**
* @return SearchRequestBuilder
*/
protected function getSearchRequestBuilder()
{
if ($this->searchRequestBuilder === null) {
$this->searchRequestBuilder = GeneralUtility::makeInstance(SearchRequestBuilder::class, /** @scrutinizer ignore-type */ $this->typoScriptConfiguration);
}
return $this->searchRequestBuilder;
}
/**
* Called when the solr server is unavailable.
*
* @return void
*/
protected function handleSolrUnavailable()
{
if ($this->typoScriptConfiguration->getLoggingExceptions()) {
/** @var SolrLogManager $logger */
$logger = GeneralUtility::makeInstance(SolrLogManager::class, /** @scrutinizer ignore-type */ __CLASS__);
$logger->log(SolrLogManager::ERROR, 'Solr server is not available');
}
}
/**
* Emits signal for various actions
*
* @param string $className Name of the class containing the signal
* @param string $signalName Name of the signal slot
* @param array $signalArguments arguments for the signal slot
*
* @return array
*/
protected function emitActionSignal($className, $signalName, array $signalArguments)
{
return $this->signalSlotDispatcher->dispatch($className, $signalName, $signalArguments)[0];
}
}

View File

@@ -0,0 +1,214 @@
<?php
namespace WapplerSystems\Meilisearch\Controller\Backend;
/***************************************************************
* Copyright notice
*
* (c) 2012-2015 Ingo Renner <ingo@typo3.org>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project is
* free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Reflection\ObjectAccess;
use TYPO3\CMS\Fluid\View\StandaloneView;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Backend\View\PageLayoutView;
/**
* Summary to display flexform settings in the page layout backend module.
*
* @author Ingo Renner <ingo@typo3.org>
* @author Timo Hund <timo.hund@dkd.de>
*/
class PageModuleSummary
{
/**
* PageLayoutView
*
* @var PageLayoutView
*/
protected $pageLayoutView;
/**
* @var array
*/
protected $pluginContentElement = [];
/**
* @var array
*/
protected $flexformData = [];
/**
* @var array
*/
protected $settings = [];
/**
* Returns information about a plugin's flexform configuration
*
* @param array $parameters Parameters to the hook
* @return string Plugin configuration information
*/
public function getSummary(array $parameters)
{
$this->initialize($parameters['row'], $parameters['pObj']);
$this->addTargetPage();
$this->addSettingFromFlexForm('Filter', 'search.query.filter');
$this->addSettingFromFlexForm('Sorting', 'search.query.sortBy');
$this->addSettingFromFlexForm('Results per Page', 'search.results.resultsPerPage');
$this->addSettingFromFlexForm('Boost Function', 'search.query.boostFunction');
$this->addSettingFromFlexForm('Boost Query', 'search.query.boostQuery');
$this->addSettingFromFlexForm('Tie Breaker', 'search.query.tieParameter');
$this->addSettingFromFlexForm('Template', 'view.templateFiles.results');
return $this->render();
}
/**
* @param array $contentElement
* @param PageLayoutView $pObj
*/
protected function initialize(array $contentElement, PageLayoutView $pObj)
{
$this->pageLayoutView = $pObj;
/** @var $service \TYPO3\CMS\Core\Service\FlexFormService::class */
$service = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Service\FlexFormService::class);
$this->flexformData = $service->convertFlexFormContentToArray($contentElement['pi_flexform']);
$this->pluginContentElement = $contentElement;
}
/**
* Adds the target page to the settings.
*/
protected function addTargetPage()
{
$targetPageId = $this->getFieldFromFlexform('search.targetPage');
if (!empty($targetPageId)) {
$page = BackendUtility::getRecord('pages', $targetPageId, 'title');
$this->settings['Target Page'] = '[' . (int)$targetPageId . '] ' . $page['title'];
}
}
/**
* @param string $settingName
* @param string $flexFormField
*/
protected function addSettingFromFlexForm($settingName, $flexFormField)
{
$value = $this->getFieldFromFlexform($flexFormField);
if (is_array($value)) {
$value = $this->addSettingFromFlexFormArray($settingName, $value);
}
$this->addSettingIfNotEmpty($settingName, (string)$value);
}
/**
* @param string $settingName
* @param array $values
* @return bool
*/
protected function addSettingFromFlexFormArray($settingName, $values)
{
foreach ($values as $item) {
if (!isset($item['field'])) {
continue;
}
$field = $item['field'];
$label = $settingName . ' ';
$label .= isset($field['field']) ? $field['field'] : '';
$fieldValue = isset($field['value']) ? $field['value'] : '';
$this->addSettingIfNotEmpty($label, (string)$fieldValue);
}
}
/**
* @param string $settingName
* @param string $value
*/
protected function addSettingIfNotEmpty($settingName, $value)
{
if (!empty($value)) {
$this->settings[$settingName] = $value;
}
}
/**
* Gets a field's value from flexform configuration, will check if
* flexform configuration is available.
*
* @param string $path name of the field
* @return string if nothing found, value if found
*/
protected function getFieldFromFlexform($path)
{
return ObjectAccess::getPropertyPath($this->flexformData, $path);
}
/**
* @return string
*/
protected function render()
{
/** @var $standaloneView StandaloneView */
$standaloneView = GeneralUtility::makeInstance(StandaloneView::class);
$standaloneView->setTemplatePathAndFilename(
GeneralUtility::getFileAbsFileName('EXT:meilisearch/Resources/Private/Templates/Backend/PageModule/Summary.html')
);
$standaloneView->assignMultiple([
'pluginLabel' => $this->getPluginLabel(),
'hidden' => $this->pluginContentElement['hidden'],
'settings' => $this->settings,
]);
return $standaloneView->render();
}
/**
* Returns the plugin label
*
* @return string
*/
protected function getPluginLabel()
{
$label = BackendUtility::getLabelFromItemListMerged($this->pluginContentElement['pid'], 'tt_content', 'list_type', $this->pluginContentElement['list_type']);
if (!empty($label)) {
$label = $this->getLanguageService()->sL($label);
} else {
$label = sprintf($this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.noMatchingValue'), $this->pluginContentElement['list_type']);
}
return $this->pageLayoutView->linkEditContent(htmlspecialchars($label), $this->pluginContentElement);
}
/**
* Returns the language service
*
* @return LanguageService
*/
protected function getLanguageService(): LanguageService
{
return $GLOBALS['LANG'];
}
}

View File

@@ -0,0 +1,328 @@
<?php
namespace WapplerSystems\Meilisearch\Controller\Backend\Search;
/***************************************************************
* Copyright notice
*
* (c) 2010-2017 dkd Internet Service GmbH <solr-support@dkd.de>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project is
* free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use WapplerSystems\Meilisearch\ConnectionManager;
use WapplerSystems\Meilisearch\Domain\Site\SiteRepository;
use WapplerSystems\Meilisearch\Domain\Site\Site;
use WapplerSystems\Meilisearch\System\Solr\SolrConnection as SolrCoreConnection;
use WapplerSystems\Meilisearch\System\Mvc\Backend\Component\Exception\InvalidViewObjectNameException;
use WapplerSystems\Meilisearch\System\Mvc\Backend\Service\ModuleDataStorageService;
use TYPO3\CMS\Backend\Template\Components\Menu\Menu;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Backend\View\BackendTemplateView;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Messaging\AbstractMessage;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
use TYPO3\CMS\Extbase\Mvc\View\NotFoundView;
use TYPO3\CMS\Extbase\Mvc\View\ViewInterface;
use TYPO3\CMS\Extbase\Utility\LocalizationUtility;
/**
* Abstract Module
*
* @property BackendTemplateView $view
*/
abstract class AbstractModuleController extends ActionController
{
/**
* Backend Template Container
*
* @var string
*/
protected $defaultViewObjectName = BackendTemplateView::class;
/**
* In the pagetree selected page UID
*
* @var int
*/
protected $selectedPageUID;
/**
* Holds the requested page UID because the selected page uid,
* might be overwritten by the automatic site selection.
*
* @var int
*/
protected $requestedPageUID;
/**
* @var Site
*/
protected $selectedSite;
/**
* @var SiteRepository
*/
protected $siteRepository;
/**
* @var SolrCoreConnection
*/
protected $selectedSolrCoreConnection;
/**
* @var Menu
*/
protected $coreSelectorMenu = null;
/**
* @var ConnectionManager
*/
protected $solrConnectionManager = null;
/**
* @var ModuleDataStorageService
*/
protected $moduleDataStorageService = null;
/**
* @param Site $selectedSite
*/
public function setSelectedSite(Site $selectedSite)
{
$this->selectedSite = $selectedSite;
}
/**
* @param SiteRepository $siteRepository
*/
public function injectSiteRepository(SiteRepository $siteRepository)
{
$this->siteRepository = $siteRepository;
}
/**
* Initializes the controller and sets needed vars.
*/
protected function initializeAction()
{
parent::initializeAction();
$this->solrConnectionManager = GeneralUtility::makeInstance(ConnectionManager::class);
$this->moduleDataStorageService = GeneralUtility::makeInstance(ModuleDataStorageService::class);
$this->selectedPageUID = (int)GeneralUtility::_GP('id');
if ($this->request->hasArgument('id')) {
$this->selectedPageUID = (int)$this->request->getArgument('id');
}
$this->requestedPageUID = $this->selectedPageUID;
if ($this->autoSelectFirstSiteAndRootPageWhenOnlyOneSiteIsAvailable()) {
return;
}
if ($this->selectedPageUID < 1) {
return;
}
try {
$this->selectedSite = $this->siteRepository->getSiteByPageId($this->selectedPageUID);
} catch (\InvalidArgumentException $exception) {
return;
}
}
/**
* @return bool
* @throws \Exception
*/
protected function autoSelectFirstSiteAndRootPageWhenOnlyOneSiteIsAvailable(): bool
{
if (count($this->siteRepository->getAvailableSites()) == 1) {
$this->selectedSite = $this->siteRepository->getFirstAvailableSite();
// we only overwrite the selected pageUid when no id was passed
if ($this->selectedPageUID === 0) {
$this->selectedPageUID = $this->selectedSite->getRootPageId();
}
return true;
}
return false;
}
/**
* Set up the doc header properly here
*
* @param ViewInterface $view
* @return void
*/
protected function initializeView(ViewInterface $view)
{
parent::initializeView($view);
$sites = $this->siteRepository->getAvailableSites();
$selectOtherPage = count($sites) > 0 || $this->selectedPageUID < 1;
$this->view->assign('showSelectOtherPage', $selectOtherPage);
$this->view->assign('pageUID', $this->selectedPageUID);
if ($view instanceof NotFoundView || $this->selectedPageUID < 1) {
return;
}
$this->view->getModuleTemplate()->addJavaScriptCode('mainJsFunctions', '
top.fsMod.recentIds["searchbackend"] = ' . (int)$this->selectedPageUID . ';'
);
if (null === $this->selectedSite) {
return;
}
/* @var BackendUserAuthentication $beUser */
$beUser = $GLOBALS['BE_USER'];
$permissionClause = $beUser->getPagePermsClause(1);
$pageRecord = BackendUtility::readPageAccess($this->selectedSite->getRootPageId(), $permissionClause);
if (false === $pageRecord) {
throw new \InvalidArgumentException(vsprintf('There is something wrong with permissions for page "%s" for backend user "%s".', [$this->selectedSite->getRootPageId(), $beUser->user['username']]), 1496146317);
}
$this->view->getModuleTemplate()->getDocHeaderComponent()->setMetaInformation($pageRecord);
}
/**
* Generates selector menu in backends doc header using selected page from page tree.
*
* @param string|null $uriToRedirectTo
*/
public function generateCoreSelectorMenuUsingPageTree(string $uriToRedirectTo = null)
{
if ($this->selectedPageUID < 1 || null === $this->selectedSite) {
return;
}
if ($this->view instanceof NotFoundView) {
$this->initializeSelectedSolrCoreConnection();
return;
}
$this->generateCoreSelectorMenu($this->selectedSite, $uriToRedirectTo);
}
/**
* Generates Core selector Menu for given Site.
*
* @param Site $site
* @param string|null $uriToRedirectTo
* @throws InvalidViewObjectNameException
*/
protected function generateCoreSelectorMenu(Site $site, string $uriToRedirectTo = null)
{
if (!$this->view instanceof BackendTemplateView) {
throw new InvalidViewObjectNameException(vsprintf(
'The controller "%s" must use BackendTemplateView to be able to generate menu for backends docheader. \
Please set `protected $defaultViewObjectName = BackendTemplateView::class;` field in your controller.',
[static::class]), 1493804179);
}
$this->view->getModuleTemplate()->setFlashMessageQueue($this->controllerContext->getFlashMessageQueue());
$this->coreSelectorMenu = $this->view->getModuleTemplate()->getDocHeaderComponent()->getMenuRegistry()->makeMenu();
$this->coreSelectorMenu->setIdentifier('component_core_selector_menu');
if (!isset($uriToRedirectTo)) {
$uriToRedirectTo = $this->uriBuilder->reset()->uriFor();
}
$this->initializeSelectedSolrCoreConnection();
$cores = $this->solrConnectionManager->getConnectionsBySite($site);
foreach ($cores as $core) {
$coreAdmin = $core->getAdminService();
$menuItem = $this->coreSelectorMenu->makeMenuItem();
$menuItem->setTitle($coreAdmin->getCorePath());
$uri = $this->uriBuilder->reset()->uriFor('switchCore',
[
'corePath' => $coreAdmin->getCorePath(),
'uriToRedirectTo' => $uriToRedirectTo
]
);
$menuItem->setHref($uri);
if ($coreAdmin->getCorePath() == $this->selectedSolrCoreConnection->getAdminService()->getCorePath()) {
$menuItem->setActive(true);
}
$this->coreSelectorMenu->addMenuItem($menuItem);
}
$this->view->getModuleTemplate()->getDocHeaderComponent()->getMenuRegistry()->addMenu($this->coreSelectorMenu);
}
/**
* Switches used core.
*
* Note: Does not check availability of core in site. All this stuff is done in the generation step.
*
* @param string $corePath
* @param string $uriToRedirectTo
*/
public function switchCoreAction(string $corePath, string $uriToRedirectTo)
{
$moduleData = $this->moduleDataStorageService->loadModuleData();
$moduleData->setCore($corePath);
$this->moduleDataStorageService->persistModuleData($moduleData);
$message = LocalizationUtility::translate('coreselector_switched_successfully', 'solr', [$corePath]);
$this->addFlashMessage($message);
$this->redirectToUri($uriToRedirectTo);
}
/**
* Initializes the solr core connection considerately to the components state.
* Uses and persists default core connection if persisted core in Site does not exist.
*
*/
private function initializeSelectedSolrCoreConnection()
{
$moduleData = $this->moduleDataStorageService->loadModuleData();
$solrCoreConnections = $this->solrConnectionManager->getConnectionsBySite($this->selectedSite);
$currentSolrCorePath = $moduleData->getCore();
if (empty($currentSolrCorePath)) {
$this->initializeFirstAvailableSolrCoreConnection($solrCoreConnections, $moduleData);
return;
}
foreach ($solrCoreConnections as $solrCoreConnection) {
if ($solrCoreConnection->getAdminService()->getCorePath() == $currentSolrCorePath) {
$this->selectedSolrCoreConnection = $solrCoreConnection;
}
}
if (!$this->selectedSolrCoreConnection instanceof SolrCoreConnection && count($solrCoreConnections) > 0) {
$this->initializeFirstAvailableSolrCoreConnection($solrCoreConnections, $moduleData);
$message = LocalizationUtility::translate('coreselector_switched_to_default_core', 'solr', [$currentSolrCorePath, $this->selectedSite->getLabel(), $this->selectedSolrCoreConnection->getAdminService()->getCorePath()]);
$this->addFlashMessage($message, '', AbstractMessage::NOTICE);
}
}
/**
* @param SolrCoreConnection[] $solrCoreConnections
*/
private function initializeFirstAvailableSolrCoreConnection(array $solrCoreConnections, $moduleData)
{
if (empty($solrCoreConnections)) {
return;
}
$this->selectedSolrCoreConnection = $solrCoreConnections[0];
$moduleData->setCore($this->selectedSolrCoreConnection->getAdminService()->getCorePath());
$this->moduleDataStorageService->persistModuleData($moduleData);
}
}

View File

@@ -0,0 +1,386 @@
<?php
namespace WapplerSystems\Meilisearch\Controller\Backend\Search;
/***************************************************************
* Copyright notice
*
* (c) 2010-2017 dkd Internet Service GmbH <solrs-support@dkd.de>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project is
* free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use WapplerSystems\Meilisearch\Utility\ManagedResourcesUtility;
use TYPO3\CMS\Backend\Template\ModuleTemplate;
use TYPO3\CMS\Core\Messaging\FlashMessage;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Mvc\View\ViewInterface;
/**
* Manage Synonyms and Stop words in Backend Module
* @property \TYPO3\CMS\Extbase\Mvc\Web\Response $response
*/
class CoreOptimizationModuleController extends AbstractModuleController
{
/**
* Set up the doc header properly here
*
* @param ViewInterface $view
* @return void
*/
protected function initializeView(ViewInterface $view)
{
parent::initializeView($view);
$this->generateCoreSelectorMenuUsingPageTree();
/* @var ModuleTemplate $module */ // holds the state of chosen tab
$module = $this->objectManager->get(ModuleTemplate::class);
$coreOptimizationTabs = $module->getDynamicTabMenu([], 'coreOptimization');
$this->view->assign('tabs', $coreOptimizationTabs);
}
/**
* Gets synonyms and stopwords for the currently selected core
*
* @return void
*/
public function indexAction()
{
if ($this->selectedSolrCoreConnection === null) {
$this->view->assign('can_not_proceed', true);
return;
}
$synonyms = [];
$coreAdmin = $this->selectedSolrCoreConnection->getAdminService();
$rawSynonyms = $coreAdmin->getSynonyms();
foreach ($rawSynonyms as $baseWord => $synonymList) {
$synonyms[$baseWord] = implode(', ', $synonymList);
}
$stopWords = $coreAdmin->getStopWords();
$this->view->assignMultiple([
'synonyms' => $synonyms,
'stopWords' => implode(PHP_EOL, $stopWords),
'stopWordsCount' => count($stopWords)
]);
}
/**
* Add synonyms to selected core
*
* @param string $baseWord
* @param string $synonyms
* @param bool $overrideExisting
* @return void
*/
public function addSynonymsAction(string $baseWord, string $synonyms, $overrideExisting)
{
if (empty($baseWord) || empty($synonyms)) {
$this->addFlashMessage(
'Please provide a base word and synonyms.',
'Missing parameter',
FlashMessage::ERROR
);
} else {
$baseWord = mb_strtolower($baseWord);
$synonyms = mb_strtolower($synonyms);
$coreAdmin = $this->selectedSolrCoreConnection->getAdminService();
if ($overrideExisting && $coreAdmin->getSynonyms($baseWord)) {
$coreAdmin->deleteSynonym($baseWord);
}
$coreAdmin->addSynonym($baseWord, GeneralUtility::trimExplode(',', $synonyms, true));
$coreAdmin->reloadCore();
$this->addFlashMessage(
'"' . $synonyms . '" added as synonyms for base word "' . $baseWord . '"'
);
}
$this->redirect('index');
}
/**
* @param string $fileFormat
* @return void
*/
public function exportStopWordsAction($fileFormat = 'txt')
{
$coreAdmin = $this->selectedSolrCoreConnection->getAdminService();
$this->exportFile(
implode(PHP_EOL, $coreAdmin->getStopWords()),
'stopwords',
$fileFormat
);
}
/**
* Exports synonyms to a download file.
*
* @param string $fileFormat
* @return string
*/
public function exportSynonymsAction($fileFormat = 'txt')
{
$coreAdmin = $this->selectedSolrCoreConnection->getAdminService();
$synonyms = $coreAdmin->getSynonyms();
return $this->exportFile(ManagedResourcesUtility::exportSynonymsToTxt($synonyms), 'synonyms', $fileFormat);
}
/**
* @param array $synonymFileUpload
* @param bool $overrideExisting
* @param bool $deleteSynonymsBefore
* @return void
*/
public function importSynonymListAction(array $synonymFileUpload, $overrideExisting, $deleteSynonymsBefore)
{
if ($deleteSynonymsBefore) {
$this->deleteAllSynonyms();
}
$fileLines = ManagedResourcesUtility::importSynonymsFromPlainTextContents($synonymFileUpload);
$synonymCount = 0;
$coreAdmin = $this->selectedSolrCoreConnection->getAdminService();
foreach ($fileLines as $baseWord => $synonyms) {
if (!isset($baseWord) || empty($synonyms)) {
continue;
}
$this->deleteExistingSynonym($overrideExisting, $deleteSynonymsBefore, $baseWord);
$coreAdmin->addSynonym($baseWord, $synonyms);
$synonymCount++;
}
$coreAdmin->reloadCore();
$this->addFlashMessage(
$synonymCount . ' synonyms imported.'
);
$this->redirect('index');
}
/**
* @param array $stopwordsFileUpload
* @param bool $replaceStopwords
* @return void
*/
public function importStopWordListAction(array $stopwordsFileUpload, $replaceStopwords)
{
$this->saveStopWordsAction(
ManagedResourcesUtility::importStopwordsFromPlainTextContents($stopwordsFileUpload),
$replaceStopwords
);
}
/**
* Delete complete synonym list
*
* @return void
*/
public function deleteAllSynonymsAction()
{
$allSynonymsCouldBeDeleted = $this->deleteAllSynonyms();
$coreAdmin = $this->selectedSolrCoreConnection->getAdminService();
$reloadResponse = $coreAdmin->reloadCore();
if ($allSynonymsCouldBeDeleted
&& $reloadResponse->getHttpStatus() == 200
) {
$this->addFlashMessage(
'All synonym removed.'
);
} else {
$this->addFlashMessage(
'Failed to remove all synonyms.',
'An error occurred',
FlashMessage::ERROR
);
}
$this->redirect('index');
}
/**
* Deletes a synonym mapping by its base word.
*
* @param string $baseWord Synonym mapping base word
*/
public function deleteSynonymsAction($baseWord)
{
$coreAdmin = $this->selectedSolrCoreConnection->getAdminService();
$deleteResponse = $coreAdmin->deleteSynonym($baseWord);
$reloadResponse = $coreAdmin->reloadCore();
if ($deleteResponse->getHttpStatus() == 200
&& $reloadResponse->getHttpStatus() == 200
) {
$this->addFlashMessage(
'Synonym removed.'
);
} else {
$this->addFlashMessage(
'Failed to remove synonym.',
'An error occurred',
FlashMessage::ERROR
);
}
$this->redirect('index');
}
/**
* Saves the edited stop word list to Solr
*
* @param string $stopWords
* @param bool $replaceStopwords
* @return void
*/
public function saveStopWordsAction(string $stopWords, $replaceStopwords = true)
{
// lowercase stopword before saving because terms get lowercased before stopword filtering
$newStopWords = mb_strtolower($stopWords);
$newStopWords = GeneralUtility::trimExplode("\n", $newStopWords, true);
$coreAdmin = $this->selectedSolrCoreConnection->getAdminService();
$oldStopWords = $coreAdmin->getStopWords();
if ($replaceStopwords) {
$removedStopWords = array_diff($oldStopWords, $newStopWords);
$wordsRemoved = $this->removeStopsWordsFromIndex($removedStopWords);
} else {
$wordsRemoved = true;
}
$wordsAdded = true;
$addedStopWords = array_diff($newStopWords, $oldStopWords);
if (!empty($addedStopWords)) {
$wordsAddedResponse = $coreAdmin->addStopWords($addedStopWords);
$wordsAdded = ($wordsAddedResponse->getHttpStatus() == 200);
}
$reloadResponse = $coreAdmin->reloadCore();
if ($wordsRemoved && $wordsAdded && $reloadResponse->getHttpStatus() == 200) {
$this->addFlashMessage(
'Stop Words Updated.'
);
}
$this->redirect('index');
}
/**
* @param string $content
* @param string $type
* @param string $fileExtension
* @return string
*/
protected function exportFile($content, $type = 'synonyms', $fileExtension = 'txt') : string
{
$coreAdmin = $this->selectedSolrCoreConnection->getAdminService();
$this->response->setHeader('Content-type', 'text/plain', true);
$this->response->setHeader('Cache-control', 'public', true);
$this->response->setHeader('Content-Description', 'File transfer', true);
$this->response->setHeader(
'Content-disposition',
'attachment; filename =' . $type . '_' .
$coreAdmin->getPrimaryEndpoint()->getCore() . '.' . $fileExtension,
true
);
$this->response->setContent($content);
$this->sendFileResponse();
}
/**
* This method send the headers and content and does an exit, since without the exit TYPO3 produces and error.
* @return void
*/
protected function sendFileResponse()
{
$this->response->sendHeaders();
$this->response->send();
$this->response->shutdown();
exit();
}
/**
* Delete complete synonym list form solr
*
* @return bool
*/
protected function deleteAllSynonyms() : bool
{
$coreAdmin = $this->selectedSolrCoreConnection->getAdminService();
$synonyms = $coreAdmin->getSynonyms();
$allSynonymsCouldBeDeleted = true;
foreach ($synonyms as $baseWord => $synonym) {
$deleteResponse = $coreAdmin->deleteSynonym($baseWord);
$allSynonymsCouldBeDeleted = $allSynonymsCouldBeDeleted && $deleteResponse->getHttpStatus() == 200;
}
return $allSynonymsCouldBeDeleted;
}
/**
* @param $stopwordsToRemove
* @return bool
*/
protected function removeStopsWordsFromIndex($stopwordsToRemove) : bool
{
$wordsRemoved = true;
$coreAdmin = $this->selectedSolrCoreConnection->getAdminService();
foreach ($stopwordsToRemove as $word) {
$response = $coreAdmin->deleteStopWord($word);
if ($response->getHttpStatus() != 200) {
$wordsRemoved = false;
$this->addFlashMessage(
'Failed to remove stop word "' . $word . '".',
'An error occurred',
FlashMessage::ERROR
);
break;
}
}
return $wordsRemoved;
}
/**
* Delete synonym entry if selceted before
* @param bool $overrideExisting
* @param bool $deleteSynonymsBefore
* @param string $baseWord
*/
protected function deleteExistingSynonym($overrideExisting, $deleteSynonymsBefore, $baseWord)
{
$coreAdmin = $this->selectedSolrCoreConnection->getAdminService();
if (!$deleteSynonymsBefore &&
$overrideExisting &&
$coreAdmin->getSynonyms($baseWord)
) {
$coreAdmin->deleteSynonym($baseWord);
}
}
}

View File

@@ -0,0 +1,195 @@
<?php
namespace WapplerSystems\Meilisearch\Controller\Backend\Search;
/***************************************************************
* Copyright notice
*
* (c) 2013-2015 Ingo Renner <ingo@typo3.org>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project is
* free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use WapplerSystems\Meilisearch\ConnectionManager;
use WapplerSystems\Meilisearch\IndexQueue\Queue;
use WapplerSystems\Meilisearch\System\Solr\SolrConnection;
use WapplerSystems\Meilisearch\Util;
use TYPO3\CMS\Backend\Routing\UriBuilder as BackendUriBuilder;
use TYPO3\CMS\Core\Messaging\FlashMessage;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Mvc\Web\ReferringRequest;
use TYPO3\CMS\Extbase\Utility\LocalizationUtility;
/**
* Index Administration Module
*
* @author Ingo Renner <ingo@typo3.org>
*/
class IndexAdministrationModuleController extends AbstractModuleController
{
/**
* @var Queue
*/
protected $indexQueue;
/**
* @var ConnectionManager
*/
protected $solrConnectionManager = null;
/**
* @param ConnectionManager $solrConnectionManager
*/
public function setSolrConnectionManager(ConnectionManager $solrConnectionManager)
{
$this->solrConnectionManager = $solrConnectionManager;
}
/**
* Initializes the controller before invoking an action method.
*/
protected function initializeAction()
{
parent::initializeAction();
$this->indexQueue = GeneralUtility::makeInstance(Queue::class);
$this->solrConnectionManager = GeneralUtility::makeInstance(ConnectionManager::class);
}
/**
* Index action, shows an overview of available index maintenance operations.
*
* @return void
*/
public function indexAction()
{
if ($this->selectedSite === null || empty($this->solrConnectionManager->getConnectionsBySite($this->selectedSite))) {
$this->view->assign('can_not_proceed', true);
}
}
/**
* Empties the site's indexes.
*
* @return void
*/
public function emptyIndexAction()
{
$siteHash = $this->selectedSite->getSiteHash();
try {
$affectedCores = [];
$solrServers = $this->solrConnectionManager->getConnectionsBySite($this->selectedSite);
foreach ($solrServers as $solrServer) {
$writeService = $solrServer->getWriteService();
/* @var $solrServer SolrConnection */
$writeService->deleteByQuery('siteHash:' . $siteHash);
$writeService->commit(false, false, false);
$affectedCores[] = $writeService->getPrimaryEndpoint()->getCore();
}
$message = LocalizationUtility::translate('solr.backend.index_administration.index_emptied_all', 'Solr', [$this->selectedSite->getLabel(), implode(', ', $affectedCores)]);
$this->addFlashMessage($message);
} catch (\Exception $e) {
$this->addFlashMessage(LocalizationUtility::translate('solr.backend.index_administration.error.on_empty_index', 'Solr', [$e->__toString()]), '', FlashMessage::ERROR);
}
$this->redirect('index');
}
/**
* Empties the Index Queue
*
* @return void
*/
public function clearIndexQueueAction()
{
$this->indexQueue->deleteItemsBySite($this->selectedSite);
$this->addFlashMessage(
LocalizationUtility::translate('solr.backend.index_administration.success.queue_emptied', 'Solr',
[$this->selectedSite->getLabel()])
);
$this->redirectToReferrerModule();
}
/**
* Reloads the site's Solr cores.
*
* @return void
*/
public function reloadIndexConfigurationAction()
{
$coresReloaded = true;
$reloadedCores = [];
$solrServers = $this->solrConnectionManager->getConnectionsBySite($this->selectedSite);
foreach ($solrServers as $solrServer) {
/* @var $solrServer SolrConnection */
$coreAdmin = $solrServer->getAdminService();
$coreReloaded = $coreAdmin->reloadCore()->getHttpStatus() === 200;
$coreName = $coreAdmin->getPrimaryEndpoint()->getCore();
if (!$coreReloaded) {
$coresReloaded = false;
$this->addFlashMessage(
'Failed to reload index configuration for core "' . $coreName . '"',
'',
FlashMessage::ERROR
);
break;
}
$reloadedCores[] = $coreName;
}
if ($coresReloaded) {
$this->addFlashMessage(
'Core configuration reloaded (' . implode(', ', $reloadedCores) . ').',
'',
FlashMessage::OK
);
}
$this->redirect('index');
}
/**
* Redirects to the referrer module index Action.
*
* Fluids <f:form VH can not make urls to other modules properly.
* The module name/key is not provided in the hidden fields __referrer by bulding form.
* So this is currently the single way to make it possible.
*
* @todo: remove this method if f:form works properly between backend modules.
*/
protected function redirectToReferrerModule()
{
$wasFromQueue = $this->request->hasArgument('fromQueue');
if (!$wasFromQueue) {
$this->redirect('index');
return;
}
/* @var BackendUriBuilder $backendUriBuilder */
$backendUriBuilder = GeneralUtility::makeInstance(BackendUriBuilder::class);
$parameters = ['id' => $this->selectedPageUID];
$referringUri = $backendUriBuilder->buildUriFromRoute('searchbackend_SolrIndexqueue', $parameters);
$this->redirectToUri($referringUri);
}
}

View File

@@ -0,0 +1,287 @@
<?php
namespace WapplerSystems\Meilisearch\Controller\Backend\Search;
/***************************************************************
* Copyright notice
*
* (c) 2013-2015 Ingo Renner <ingo@typo3.org>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project is
* free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use WapplerSystems\Meilisearch\Backend\IndexingConfigurationSelectorField;
use WapplerSystems\Meilisearch\Domain\Index\IndexService;
use WapplerSystems\Meilisearch\IndexQueue\Queue;
use TYPO3\CMS\Backend\View\BackendTemplateView;
use TYPO3\CMS\Core\Messaging\FlashMessage;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Mvc\View\ViewInterface;
use TYPO3\CMS\Extbase\Utility\LocalizationUtility;
/**
* Index Queue Module
*
* @author Ingo Renner <ingo@typo3.org>
* @property BackendTemplateView $view
*/
class IndexQueueModuleController extends AbstractModuleController
{
/**
* Module name, used to identify a module f.e. in URL parameters.
*
* @var string
*/
protected $moduleName = 'IndexQueue';
/**
* Module title, shows up in the module menu.
*
* @var string
*/
protected $moduleTitle = 'Index Queue';
/**
* @var Queue
*/
protected $indexQueue;
/**
* Initializes the controller before invoking an action method.
*/
protected function initializeAction()
{
parent::initializeAction();
$this->indexQueue = GeneralUtility::makeInstance(Queue::class);
}
/**
* @param Queue $indexQueue
*/
public function setIndexQueue(Queue $indexQueue)
{
$this->indexQueue = $indexQueue;
}
/**
* Set up the doc header properly here
*
* @param ViewInterface $view
* @return void
*/
protected function initializeView(ViewInterface $view)
{
parent::initializeView($view);
}
/**
* Lists the available indexing configurations
*
* @return void
*/
public function indexAction()
{
if (!$this->canQueueSelectedSite()) {
$this->view->assign('can_not_proceed', true);
return;
}
$statistics = $this->indexQueue->getStatisticsBySite($this->selectedSite);
$this->view->assign('indexQueueInitializationSelector', $this->getIndexQueueInitializationSelector());
$this->view->assign('indexqueue_statistics', $statistics);
$this->view->assign('indexqueue_errors', $this->indexQueue->getErrorsBySite($this->selectedSite));
}
/**
* Checks if selected site can be queued.
*
* @return bool
*/
protected function canQueueSelectedSite()
{
if ($this->selectedSite === null || empty($this->solrConnectionManager->getConnectionsBySite($this->selectedSite))) {
return false;
}
$enabledIndexQueueConfigurationNames = $this->selectedSite->getSolrConfiguration()->getEnabledIndexQueueConfigurationNames();
if (empty($enabledIndexQueueConfigurationNames)) {
return false;
}
return true;
}
/**
* Renders a field to select which indexing configurations to initialize.
*
* Uses TCEforms.
*
* @return string Markup for the select field
*/
protected function getIndexQueueInitializationSelector()
{
$selector = GeneralUtility::makeInstance(IndexingConfigurationSelectorField::class, /** @scrutinizer ignore-type */ $this->selectedSite);
$selector->setFormElementName('tx_meilisearch-index-queue-initialization');
return $selector->render();
}
/**
* Initializes the Index Queue for selected indexing configurations
*
* @return void
*/
public function initializeIndexQueueAction()
{
$initializedIndexingConfigurations = [];
$indexingConfigurationsToInitialize = GeneralUtility::_POST('tx_meilisearch-index-queue-initialization');
if ((!empty($indexingConfigurationsToInitialize)) && (is_array($indexingConfigurationsToInitialize))) {
// initialize selected indexing configuration
$initializedIndexingConfigurations = $this->indexQueue->getInitializationService()->initializeBySiteAndIndexConfigurations($this->selectedSite, $indexingConfigurationsToInitialize);
} else {
$messageLabel = 'solr.backend.index_queue_module.flashmessage.initialize.no_selection';
$titleLabel = 'solr.backend.index_queue_module.flashmessage.not_initialized.title';
$this->addFlashMessage(
LocalizationUtility::translate($messageLabel, 'Solr'),
LocalizationUtility::translate($titleLabel, 'Solr'),
FlashMessage::WARNING
);
}
$messagesForConfigurations = [];
foreach (array_keys($initializedIndexingConfigurations) as $indexingConfigurationName) {
$itemCount = $this->indexQueue->getStatisticsBySite($this->selectedSite, $indexingConfigurationName)->getTotalCount();
$messagesForConfigurations[] = $indexingConfigurationName . ' (' . $itemCount . ' records)';
}
if (!empty($initializedIndexingConfigurations)) {
$messageLabel = 'solr.backend.index_queue_module.flashmessage.initialize.success';
$titleLabel = 'solr.backend.index_queue_module.flashmessage.initialize.title';
$this->addFlashMessage(
LocalizationUtility::translate($messageLabel, 'Solr', [implode(', ', $messagesForConfigurations)]),
LocalizationUtility::translate($titleLabel, 'Solr'),
FlashMessage::OK
);
}
$this->redirect('index');
}
/**
* Removes all errors in the index queue list. So that the items can be indexed again.
*
* @return void
*/
public function resetLogErrorsAction()
{
$resetResult = $this->indexQueue->resetAllErrors();
$label = 'solr.backend.index_queue_module.flashmessage.success.reset_errors';
$severity = FlashMessage::OK;
if (!$resetResult) {
$label = 'solr.backend.index_queue_module.flashmessage.error.reset_errors';
$severity = FlashMessage::ERROR;
}
$this->addIndexQueueFlashMessage($label, $severity);
$this->redirect('index');
}
/**
* ReQueues a single item in the indexQueue.
*
* @param string $type
* @param int $uid
*
* @return void
*/
public function requeueDocumentAction(string $type, int $uid)
{
$label = 'solr.backend.index_queue_module.flashmessage.error.single_item_not_requeued';
$severity = FlashMessage::ERROR;
$updateCount = $this->indexQueue->updateItem($type, $uid, time());
if ($updateCount > 0) {
$label = 'solr.backend.index_queue_module.flashmessage.success.single_item_was_requeued';
$severity = FlashMessage::OK;
}
$this->addIndexQueueFlashMessage($label, $severity);
$this->redirect('index');
}
/**
* Shows the error message for one queue item.
*
* @param int $indexQueueItemId
* @return void
*/
public function showErrorAction(int $indexQueueItemId)
{
if (is_null($indexQueueItemId)) {
// add a flash message and quit
$label = 'solr.backend.index_queue_module.flashmessage.error.no_queue_item_for_queue_error';
$severity = FlashMessage::ERROR;
$this->addIndexQueueFlashMessage($label, $severity);
return;
}
$item = $this->indexQueue->getItem($indexQueueItemId);
$this->view->assign('indexQueueItem', $item);
}
/**
* Indexes a few documents with the index service.
* @return void
* @throws \TYPO3\CMS\Extbase\Mvc\Exception\StopActionException
* @throws \TYPO3\CMS\Extbase\Mvc\Exception\UnsupportedRequestTypeException
*/
public function doIndexingRunAction()
{
/** @var $indexService \WapplerSystems\Meilisearch\Domain\Index\IndexService */
$indexService = GeneralUtility::makeInstance(IndexService::class, /** @scrutinizer ignore-type */ $this->selectedSite);
$indexWithoutErrors = $indexService->indexItems(10);
$label = 'solr.backend.index_queue_module.flashmessage.success.index_manual';
$severity = FlashMessage::OK;
if (!$indexWithoutErrors) {
$label = 'solr.backend.index_queue_module.flashmessage.error.index_manual';
$severity = FlashMessage::ERROR;
}
$this->addFlashMessage(
LocalizationUtility::translate($label, 'Solr'),
LocalizationUtility::translate('solr.backend.index_queue_module.flashmessage.index_manual', 'Solr'),
$severity
);
$this->redirect('index');
}
/**
* Adds a flash message for the index queue module.
*
* @param string $label
* @param int $severity
*/
protected function addIndexQueueFlashMessage($label, $severity)
{
$this->addFlashMessage(LocalizationUtility::translate($label, 'Solr'), LocalizationUtility::translate('solr.backend.index_queue_module.flashmessage.title', 'Solr'), $severity);
}
}

View File

@@ -0,0 +1,324 @@
<?php
namespace WapplerSystems\Meilisearch\Controller\Backend\Search;
/***************************************************************
* Copyright notice
*
* (c) 2010-2017 dkd Internet Service GmbH <solr-support@dkd.de>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project is
* free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use WapplerSystems\Meilisearch\Api;
use WapplerSystems\Meilisearch\ConnectionManager;
use WapplerSystems\Meilisearch\Domain\Search\Statistics\StatisticsRepository;
use WapplerSystems\Meilisearch\Domain\Search\ApacheSolrDocument\Repository;
use WapplerSystems\Meilisearch\System\Solr\ResponseAdapter;
use WapplerSystems\Meilisearch\System\Validator\Path;
use TYPO3\CMS\Backend\Template\ModuleTemplate;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Messaging\FlashMessage;
use TYPO3\CMS\Core\Registry;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Mvc\Exception\StopActionException;
use TYPO3\CMS\Extbase\Mvc\View\ViewInterface;
/**
* Info Module
*/
class InfoModuleController extends AbstractModuleController
{
/**
* @var ConnectionManager
*/
protected $solrConnectionManager;
/**
* @var Repository
*/
protected $apacheSolrDocumentRepository;
/**
* Initializes the controller before invoking an action method.
*/
protected function initializeAction()
{
parent::initializeAction();
$this->solrConnectionManager = GeneralUtility::makeInstance(ConnectionManager::class);
$this->apacheSolrDocumentRepository = GeneralUtility::makeInstance(Repository::class);
}
/**
* Set up the doc header properly here
*
* @param ViewInterface $view
* @return void
*/
protected function initializeView(ViewInterface $view)
{
parent::initializeView($view);
/* @var ModuleTemplate $module */ // holds the state of chosen tab
$module = GeneralUtility::makeInstance(ModuleTemplate::class);
$coreOptimizationTabs = $module->getDynamicTabMenu([], 'coreOptimization');
$this->view->assign('tabs', $coreOptimizationTabs);
}
/**
* Index action, shows an overview of the state of the Solr index
*
* @return void
*/
public function indexAction()
{
if ($this->selectedSite === null) {
$this->view->assign('can_not_proceed', true);
return;
}
$this->collectConnectionInfos();
$this->collectStatistics();
$this->collectIndexFieldsInfo();
$this->collectIndexInspectorInfo();
}
/**
* @param string $type
* @param int $uid
* @param int $pageId
* @param int $languageUid
* @return void
*/
public function documentsDetailsAction(string $type, int $uid, int $pageId, int $languageUid)
{
$documents = $this->apacheSolrDocumentRepository->findByTypeAndPidAndUidAndLanguageId($type, $uid, $pageId, $languageUid);
$this->view->assign('documents', $documents);
}
/**
* Checks whether the configured Solr server can be reached and provides a
* flash message according to the result of the check.
*
* @return void
*/
protected function collectConnectionInfos()
{
$connectedHosts = [];
$missingHosts = [];
$invalidPaths = [];
/* @var Path $path */
$path = GeneralUtility::makeInstance(Path::class);
$connections = $this->solrConnectionManager->getConnectionsBySite($this->selectedSite);
if (empty($connections)) {
$this->view->assign('can_not_proceed', true);
return;
}
foreach ($connections as $connection) {
$coreAdmin = $connection->getAdminService();
$coreUrl = (string)$coreAdmin;
if ($coreAdmin->ping()) {
$connectedHosts[] = $coreUrl;
} else {
$missingHosts[] = $coreUrl;
}
if (!$path->isValidSolrPath($coreAdmin->getCorePath())) {
$invalidPaths[] = $coreAdmin->getCorePath();
}
}
$this->view->assignMultiple([
'site' => $this->selectedSite,
'apiKey' => Api::getApiKey(),
'connectedHosts' => $connectedHosts,
'missingHosts' => $missingHosts,
'invalidPaths' => $invalidPaths
]);
}
/**
* Index action, shows an overview of the state of the Solr index
*
* @return void
*/
protected function collectStatistics()
{
// TODO make time frame user adjustable, for now it's last 30 days
$siteRootPageId = $this->selectedSite->getRootPageId();
/* @var StatisticsRepository $statisticsRepository */
$statisticsRepository = GeneralUtility::makeInstance(StatisticsRepository::class);
// @TODO: Do we want Typoscript constants to restrict the results?
$this->view->assign(
'top_search_phrases',
$statisticsRepository->getTopKeyWordsWithHits($siteRootPageId, 30, 5)
);
$this->view->assign(
'top_search_phrases_without_hits',
$statisticsRepository->getTopKeyWordsWithoutHits($siteRootPageId, 30, 5)
);
$this->view->assign(
'search_phrases_statistics',
$statisticsRepository->getSearchStatistics($siteRootPageId, 30, 100)
);
$labels = [];
$data = [];
$chartData = $statisticsRepository->getQueriesOverTime($siteRootPageId, 30, 86400);
foreach ($chartData as $bucket) {
$labels[] = strftime('%x', $bucket['timestamp']);
$data[] = (int)$bucket['numQueries'];
}
$this->view->assign('queriesChartLabels', json_encode($labels));
$this->view->assign('queriesChartData', json_encode($data));
}
/**
* Gets Luke meta data for the currently selected core and provides a list
* of that data.
*
* @return void
*/
protected function collectIndexFieldsInfo()
{
$indexFieldsInfoByCorePaths = [];
$solrCoreConnections = $this->solrConnectionManager->getConnectionsBySite($this->selectedSite);
foreach ($solrCoreConnections as $solrCoreConnection) {
$coreAdmin = $solrCoreConnection->getAdminService();
$indexFieldsInfo = [
'corePath' => $coreAdmin->getCorePath()
];
if ($coreAdmin->ping()) {
$lukeData = $coreAdmin->getLukeMetaData();
/* @var Registry $registry */
$registry = GeneralUtility::makeInstance(Registry::class);
$limit = $registry->get('tx_meilisearch', 'luke.limit', 20000);
$limitNote = '';
if (isset($lukeData->index->numDocs) && $lukeData->index->numDocs > $limit) {
$limitNote = '<em>Too many terms</em>';
} elseif (isset($lukeData->index->numDocs)) {
$limitNote = 'Nothing indexed';
// below limit, so we can get more data
// Note: we use 2 since 1 fails on Ubuntu Hardy.
$lukeData = $coreAdmin->getLukeMetaData(2);
}
$fields = $this->getFields($lukeData, $limitNote);
$coreMetrics = $this->getCoreMetrics($lukeData, $fields);
$indexFieldsInfo['noError'] = 'OK';
$indexFieldsInfo['fields'] = $fields;
$indexFieldsInfo['coreMetrics'] = $coreMetrics;
} else {
$indexFieldsInfo['noError'] = null;
$this->addFlashMessage(
'',
'Unable to contact Apache Solr server: ' . $this->selectedSite->getLabel() . ' ' . $coreAdmin->getCorePath(),
FlashMessage::ERROR
);
}
$indexFieldsInfoByCorePaths[$coreAdmin->getCorePath()] = $indexFieldsInfo;
}
$this->view->assign('indexFieldsInfoByCorePaths', $indexFieldsInfoByCorePaths);
}
/**
* Retrieves the information for the index inspector.
*
* @return void
*/
protected function collectIndexInspectorInfo()
{
$solrCoreConnections = $this->solrConnectionManager->getConnectionsBySite($this->selectedSite);
$documentsByCoreAndType = [];
foreach ($solrCoreConnections as $languageId => $solrCoreConnection) {
$coreAdmin = $solrCoreConnection->getAdminService();
$documents = $this->apacheSolrDocumentRepository->findByPageIdAndByLanguageId($this->selectedPageUID, $languageId);
$documentsByType = [];
foreach ($documents as $document) {
$documentsByType[$document['type']][] = $document;
}
$documentsByCoreAndType[$languageId]['core'] = $coreAdmin;
$documentsByCoreAndType[$languageId]['documents'] = $documentsByType;
}
$this->view->assignMultiple([
'pageId' => $this->selectedPageUID,
'indexInspectorDocumentsByLanguageAndType' => $documentsByCoreAndType
]);
}
/**
* Gets field metrics.
*
* @param ResponseAdapter $lukeData Luke index data
* @param string $limitNote Note to display if there are too many documents in the index to show number of terms for a field
*
* @return array An array of field metrics
*/
protected function getFields(ResponseAdapter $lukeData, $limitNote)
{
$rows = [];
$fields = (array)$lukeData->fields;
foreach ($fields as $name => $field) {
$rows[$name] = [
'name' => $name,
'type' => $field->type,
'docs' => isset($field->docs) ? $field->docs : 0,
'terms' => isset($field->distinct) ? $field->distinct : $limitNote
];
}
ksort($rows);
return $rows;
}
/**
* Gets general core metrics.
*
* @param ResponseAdapter $lukeData Luke index data
* @param array $fields Fields metrics
*
* @return array An array of core metrics
*/
protected function getCoreMetrics(ResponseAdapter $lukeData, array $fields)
{
$coreMetrics = [
'numberOfDocuments' => $lukeData->index->numDocs,
'numberOfDeletedDocuments' => $lukeData->index->deletedDocs,
'numberOfTerms' => $lukeData->index->numTerms,
'numberOfFields' => count($fields)
];
return $coreMetrics;
}
}

View File

@@ -0,0 +1,210 @@
<?php
namespace WapplerSystems\Meilisearch\Controller;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\SearchResultSet;
use WapplerSystems\Meilisearch\System\Logging\SolrLogManager;
use WapplerSystems\Meilisearch\System\Solr\SolrUnavailableException;
use WapplerSystems\Meilisearch\Util;
use TYPO3\CMS\Extbase\Mvc\View\ViewInterface;
use TYPO3\CMS\Extbase\Mvc\Web\Response;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Fluid\View\TemplateView;
/**
* Class SearchController
*
* @author Frans Saris <frans@beech.it>
* @author Timo Hund <timo.hund@dkd.de>
*/
class SearchController extends AbstractBaseController
{
/**
* @var TemplateView
*/
protected $view;
/**
* Provide search query in extbase arguments.
*/
protected function initializeAction()
{
parent::initializeAction();
$this->mapGlobalQueryStringWhenEnabled();
}
/**
* @return void
*/
protected function mapGlobalQueryStringWhenEnabled()
{
$query = GeneralUtility::_GET('q');
$useGlobalQueryString = $query !== null && !$this->typoScriptConfiguration->getSearchIgnoreGlobalQParameter();
if ($useGlobalQueryString) {
$this->request->setArgument('q', $query);
}
}
/**
* @param ViewInterface $view
*/
public function initializeView(ViewInterface $view)
{
if($view instanceof TemplateView) {
$customTemplate = $this->getCustomTemplateFromConfiguration();
if($customTemplate === '') {
return;
}
if(strpos($customTemplate, 'EXT:') !== false) {
$view->setTemplatePathAndFilename($customTemplate);
} else {
$view->setTemplate($customTemplate);
}
}
}
/**
* @return string
*/
protected function getCustomTemplateFromConfiguration()
{
$templateKey = str_replace('Action', '', $this->actionMethodName);
$customTemplate = $this->typoScriptConfiguration->getViewTemplateByFileKey($templateKey);
return $customTemplate;
}
/**
* Results
*/
public function resultsAction()
{
try {
$arguments = (array)$this->request->getArguments();
$pageId = $this->typoScriptFrontendController->getRequestedId();
$languageId = Util::getLanguageUid();
$searchRequest = $this->getSearchRequestBuilder()->buildForSearch($arguments, $pageId, $languageId);
$searchResultSet = $this->searchService->search($searchRequest);
// we pass the search result set to the controller context, to have the possibility
// to access it without passing it from partial to partial
$this->controllerContext->setSearchResultSet($searchResultSet);
$values = [
'additionalFilters' => $this->getAdditionalFilters(),
'resultSet' => $searchResultSet,
'pluginNamespace' => $this->typoScriptConfiguration->getSearchPluginNamespace(),
'arguments' => $arguments
];
$values = $this->emitActionSignal(__CLASS__, __FUNCTION__, [$values]);
$this->view->assignMultiple($values);
} catch (SolrUnavailableException $e) {
$this->handleSolrUnavailable();
}
}
/**
* Form
*/
public function formAction()
{
$values = [
'search' => $this->searchService->getSearch(),
'additionalFilters' => $this->getAdditionalFilters(),
'pluginNamespace' => $this->typoScriptConfiguration->getSearchPluginNamespace()
];
$values = $this->emitActionSignal(__CLASS__, __FUNCTION__, [$values]);
$this->view->assignMultiple($values);
}
/**
* Frequently Searched
*/
public function frequentlySearchedAction()
{
/** @var $searchResultSet SearchResultSet */
$searchResultSet = GeneralUtility::makeInstance(SearchResultSet::class);
$pageId = $this->typoScriptFrontendController->getRequestedId();
$languageId = Util::getLanguageUid();
$searchRequest = $this->getSearchRequestBuilder()->buildForFrequentSearches($pageId, $languageId);
$searchResultSet->setUsedSearchRequest($searchRequest);
$this->controllerContext->setSearchResultSet($searchResultSet);
$values = [
'additionalFilters' => $this->getAdditionalFilters(),
'resultSet' => $searchResultSet
];
$values = $this->emitActionSignal(__CLASS__, __FUNCTION__, [$values]);
$this->view->assignMultiple($values);
}
/**
* This action allows to render a detailView with data from solr.
*
* @param string $documentId
*/
public function detailAction($documentId = '')
{
try {
$document = $this->searchService->getDocumentById($documentId);
$this->view->assign('document', $document);
} catch (SolrUnavailableException $e) {
$this->handleSolrUnavailable();
}
}
/**
* Rendered when no search is available.
* @return string
*/
public function solrNotAvailableAction()
{
if ($this->response instanceof Response) {
$this->response->setStatus(503);
}
}
/**
* Called when the solr server is unavailable.
*
* @return void
*/
protected function handleSolrUnavailable()
{
parent::handleSolrUnavailable();
$this->forward('solrNotAvailable');
}
/**
* This method can be overwritten to add additionalFilters for the autosuggest.
* By default the suggest controller will apply the configured filters from the typoscript configuration.
*
* @return array
*/
protected function getAdditionalFilters()
{
return [];
}
}

View File

@@ -0,0 +1,81 @@
<?php
namespace WapplerSystems\Meilisearch\Controller;
/***************************************************************
* Copyright notice
*
* (c) 2017 Timo Hund <timo.hund@dkd.de>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project is
* free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use WapplerSystems\Meilisearch\Domain\Search\Suggest\SuggestService;
use WapplerSystems\Meilisearch\System\Solr\SolrUnavailableException;
use WapplerSystems\Meilisearch\Util;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Class SuggestController
*
* @author Frans Saris <frans@beech.it>
* @author Timo Hund <timo.hund@dkd.de>
*/
class SuggestController extends AbstractBaseController
{
/**
* This method creates a suggest json response that can be used in a suggest layer.
*
* @param string $queryString
* @param string $callback
* @param array $additionalFilters
* @return string
*/
public function suggestAction($queryString, $callback = null, $additionalFilters = [])
{
// Get suggestions
$rawQuery = htmlspecialchars(mb_strtolower(trim($queryString)));
try {
/** @var SuggestService $suggestService */
$suggestService = GeneralUtility::makeInstance(
SuggestService::class,
/** @scrutinizer ignore-type */ $this->typoScriptFrontendController,
/** @scrutinizer ignore-type */ $this->searchService,
/** @scrutinizer ignore-type */ $this->typoScriptConfiguration);
$additionalFilters = is_array($additionalFilters) ? array_map("htmlspecialchars", $additionalFilters) : [];
$pageId = $this->typoScriptFrontendController->getRequestedId();
$languageId = Util::getLanguageUid();
$arguments = (array)$this->request->getArguments();
$searchRequest = $this->getSearchRequestBuilder()->buildForSuggest($arguments, $rawQuery, $pageId, $languageId);
$result = $suggestService->getSuggestions($searchRequest, $additionalFilters);
} catch (SolrUnavailableException $e) {
$this->handleSolrUnavailable();
$result = ['status' => false];
}
if ($callback) {
return htmlspecialchars($callback) . '(' . json_encode($result) . ')';
}
else {
return json_encode($result);
}
}
}

View File

@@ -0,0 +1,111 @@
<?php declare(strict_types = 1);
namespace WapplerSystems\Meilisearch\Domain\Index\Classification;
/***************************************************************
* Copyright notice
*
* (c) 2010-2017 dkd Internet Service GmbH <solr-support@dkd.de>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project is
* free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
/**
* Class Classification
*/
class Classification {
/**
* Array of regular expressions
*
* @var array
*/
protected $matchPatterns = [];
/**
* Array of regular expressions
* @var array
*/
protected $unMatchPatterns = [];
/**
* @var string
*/
protected $mappedClass = '';
/**
* Classification constructor.
* @param array $matchPatterns
* @param array $unMatchPatterns
* @param string $mappedClass
*/
public function __construct(array $matchPatterns = [], array $unMatchPatterns = [],string $mappedClass = '')
{
$this->matchPatterns = $matchPatterns;
$this->unMatchPatterns = $unMatchPatterns;
$this->mappedClass = $mappedClass;
}
/**
* @return array
*/
public function getUnMatchPatterns(): array
{
return $this->unMatchPatterns;
}
/**
* @param array $unMatchPatterns
*/
public function setUnMatchPatterns(array $unMatchPatterns)
{
$this->unMatchPatterns = $unMatchPatterns;
}
/**
* @return array
*/
public function getMatchPatterns(): array
{
return $this->matchPatterns;
}
/**
* @param array $matchPatterns
*/
public function setMatchPatterns(array $matchPatterns)
{
$this->matchPatterns = $matchPatterns;
}
/**
* @return string
*/
public function getMappedClass(): string
{
return $this->mappedClass;
}
/**
* @param string $mappedClass
*/
public function setMappedClass(string $mappedClass)
{
$this->mappedClass = $mappedClass;
}
}

View File

@@ -0,0 +1,88 @@
<?php declare(strict_types = 1);
namespace WapplerSystems\Meilisearch\Domain\Index\Classification;
/***************************************************************
* Copyright notice
*
* (c) 2010-2017 dkd Internet Service GmbH <solr-support@dkd.de>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project is
* free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
/**
* Class ClassificationService
*/
class ClassificationService {
/**
* @param string $stringToMatch
* @param Classification[] $classifications
* @return array
*/
public function getMatchingClassNames(string $stringToMatch, $classifications) : array
{
$matchingClassification = [];
foreach ($classifications as $classification) {
$matchingClassification = $this->applyMatchPatterns($stringToMatch, $classification, $matchingClassification);
$matchingClassification = $this->applyUnMatchPatterns($stringToMatch, $classification, $matchingClassification);
}
return array_values($matchingClassification);
}
/**
* @param string $stringToMatch
* @param Classification $classification
* @param $matchingClassification
* @return array
*/
protected function applyMatchPatterns(string $stringToMatch, $classification, $matchingClassification): array
{
/** @var $classification Classification */
foreach ($classification->getMatchPatterns() as $matchPattern) {
if (preg_match_all('~' . $matchPattern . '~ims', $stringToMatch) > 0) {
$matchingClassification[] = $classification->getMappedClass();
// if we found one match, we do not need to check the other patterns
break;
}
}
return array_unique($matchingClassification);
}
/**
* @param string $stringToMatch
* @param Classification $classification
* @param $matchingClassification
* @param $messages
* @return array
*/
protected function applyUnMatchPatterns(string $stringToMatch, $classification, $matchingClassification): array
{
foreach ($classification->getUnMatchPatterns() as $unMatchPattern) {
if (preg_match_all('~' . $unMatchPattern . '~ims', $stringToMatch) > 0) {
// if we found one match, we do not need to check the other patterns
$position = array_search($classification->getMappedClass(), $matchingClassification);
if ($position !== false) {
unset($matchingClassification[$position]);
}
}
}
return $matchingClassification;
}
}

View File

@@ -0,0 +1,318 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Index;
/***************************************************************
* Copyright notice
*
* (c) 2015-2016 Timo Hund <timo.hund@dkd.de>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project is
* free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use WapplerSystems\Meilisearch\ConnectionManager;
use WapplerSystems\Meilisearch\IndexQueue\Indexer;
use WapplerSystems\Meilisearch\IndexQueue\Item;
use WapplerSystems\Meilisearch\IndexQueue\Queue;
use WapplerSystems\Meilisearch\Domain\Site\Site;
use WapplerSystems\Meilisearch\System\Configuration\TypoScriptConfiguration;
use WapplerSystems\Meilisearch\System\Logging\SolrLogManager;
use WapplerSystems\Meilisearch\Task\IndexQueueWorkerTask;
use Solarium\Exception\HttpException;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\SignalSlot\Dispatcher;
/**
* Service to perform indexing operations
*
* @author Timo Hund <timo.schmidt@dkd.de>
*/
class IndexService
{
/**
* @var TypoScriptConfiguration
*/
protected $configuration;
/**
* @var Site
*/
protected $site;
/**
* @var IndexQueueWorkerTask
*/
protected $contextTask;
/**
* @var Queue
*/
protected $indexQueue;
/**
* @var Dispatcher
*/
protected $signalSlotDispatcher;
/**
* @var \WapplerSystems\Meilisearch\System\Logging\SolrLogManager
*/
protected $logger = null;
/**
* IndexService constructor.
* @param Site $site
* @param Queue|null $queue
* @param Dispatcher|null $dispatcher
* @param SolrLogManager|null $solrLogManager
*/
public function __construct(Site $site, Queue $queue = null, Dispatcher $dispatcher = null, SolrLogManager $solrLogManager = null)
{
$this->site = $site;
$this->indexQueue = $queue ?? GeneralUtility::makeInstance(Queue::class);
$this->signalSlotDispatcher = $dispatcher ?? GeneralUtility::makeInstance(Dispatcher::class);
$this->logger = $solrLogManager ?? GeneralUtility::makeInstance(SolrLogManager::class, /** @scrutinizer ignore-type */ __CLASS__);
}
/**
* @param \WapplerSystems\Meilisearch\Task\IndexQueueWorkerTask $contextTask
*/
public function setContextTask($contextTask)
{
$this->contextTask = $contextTask;
}
/**
* @return \WapplerSystems\Meilisearch\Task\IndexQueueWorkerTask
*/
public function getContextTask()
{
return $this->contextTask;
}
/**
* Indexes items from the Index Queue.
*
* @param int $limit
* @return bool
*/
public function indexItems($limit)
{
$errors = 0;
$indexRunId = uniqid();
$configurationToUse = $this->site->getSolrConfiguration();
$enableCommitsSetting = $configurationToUse->getEnableCommits();
// get items to index
$itemsToIndex = $this->indexQueue->getItemsToIndex($this->site, $limit);
$this->emitSignal('beforeIndexItems', [$itemsToIndex, $this->getContextTask(), $indexRunId]);
foreach ($itemsToIndex as $itemToIndex) {
try {
// try indexing
$this->emitSignal('beforeIndexItem', [$itemToIndex, $this->getContextTask(), $indexRunId]);
$this->indexItem($itemToIndex, $configurationToUse);
$this->emitSignal('afterIndexItem', [$itemToIndex, $this->getContextTask(), $indexRunId]);
} catch (\Exception $e) {
$errors++;
$this->indexQueue->markItemAsFailed($itemToIndex, $e->getCode() . ': ' . $e->__toString());
$this->generateIndexingErrorLog($itemToIndex, $e);
}
}
$this->emitSignal('afterIndexItems', [$itemsToIndex, $this->getContextTask(), $indexRunId]);
if ($enableCommitsSetting && count($itemsToIndex) > 0) {
$solrServers = GeneralUtility::makeInstance(ConnectionManager::class)->getConnectionsBySite($this->site);
foreach ($solrServers as $solrServer) {
try {
$solrServer->getWriteService()->commit(false, false, false);
} catch (HttpException $e) {
$errors++;
}
}
}
return ($errors === 0);
}
/**
* Generates a message in the error log when an error occured.
*
* @param Item $itemToIndex
* @param \Exception $e
*/
protected function generateIndexingErrorLog(Item $itemToIndex, \Exception $e)
{
$message = 'Failed indexing Index Queue item ' . $itemToIndex->getIndexQueueUid();
$data = ['code' => $e->getCode(), 'message' => $e->getMessage(), 'trace' => $e->getTraceAsString(), 'item' => (array)$itemToIndex];
$this->logger->log(
SolrLogManager::ERROR,
$message,
$data
);
}
/**
* Builds an emits a singal for the IndexService.
*
* @param string $name
* @param array $arguments
* @return mixed
*/
protected function emitSignal($name, $arguments)
{
return $this->signalSlotDispatcher->dispatch(__CLASS__, $name, $arguments);
}
/**
* Indexes an item from the Index Queue.
*
* @param Item $item An index queue item to index
* @param TypoScriptConfiguration $configuration
* @return bool TRUE if the item was successfully indexed, FALSE otherwise
*/
protected function indexItem(Item $item, TypoScriptConfiguration $configuration)
{
$indexer = $this->getIndexerByItem($item->getIndexingConfigurationName(), $configuration);
// Remember original http host value
$originalHttpHost = isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : null;
$itemChangedDate = $item->getChanged();
$itemChangedDateAfterIndex = 0;
try {
$this->initializeHttpServerEnvironment($item);
$itemIndexed = $indexer->index($item);
// update IQ item so that the IQ can determine what's been indexed already
if ($itemIndexed) {
$this->indexQueue->updateIndexTimeByItem($item);
$itemChangedDateAfterIndex = $item->getChanged();
}
if ($itemChangedDateAfterIndex > $itemChangedDate && $itemChangedDateAfterIndex > time()) {
$this->indexQueue->setForcedChangeTimeByItem($item, $itemChangedDateAfterIndex);
}
} catch (\Exception $e) {
$this->restoreOriginalHttpHost($originalHttpHost);
throw $e;
}
$this->restoreOriginalHttpHost($originalHttpHost);
return $itemIndexed;
}
/**
* A factory method to get an indexer depending on an item's configuration.
*
* By default all items are indexed using the default indexer
* (WapplerSystems\Meilisearch\IndexQueue\Indexer) coming with EXT:meilisearch. Pages by default are
* configured to be indexed through a dedicated indexer
* (WapplerSystems\Meilisearch\IndexQueue\PageIndexer). In all other cases a dedicated indexer
* can be specified through TypoScript if needed.
*
* @param string $indexingConfigurationName Indexing configuration name.
* @param TypoScriptConfiguration $configuration
* @return Indexer
*/
protected function getIndexerByItem($indexingConfigurationName, TypoScriptConfiguration $configuration)
{
$indexerClass = $configuration->getIndexQueueIndexerByConfigurationName($indexingConfigurationName);
$indexerConfiguration = $configuration->getIndexQueueIndexerConfigurationByConfigurationName($indexingConfigurationName);
$indexer = GeneralUtility::makeInstance($indexerClass, /** @scrutinizer ignore-type */ $indexerConfiguration);
if (!($indexer instanceof Indexer)) {
throw new \RuntimeException(
'The indexer class "' . $indexerClass . '" for indexing configuration "' . $indexingConfigurationName . '" is not a valid indexer. Must be a subclass of WapplerSystems\Meilisearch\IndexQueue\Indexer.',
1260463206
);
}
return $indexer;
}
/**
* Gets the indexing progress.
*
* @return float Indexing progress as a two decimal precision float. f.e. 44.87
*/
public function getProgress()
{
return $this->indexQueue->getStatisticsBySite($this->site)->getSuccessPercentage();
}
/**
* Returns the amount of failed queue items for the current site.
*
* @return int
*/
public function getFailCount()
{
return $this->indexQueue->getStatisticsBySite($this->site)->getFailedCount();
}
/**
* Initializes the $_SERVER['HTTP_HOST'] environment variable in CLI
* environments dependent on the Index Queue item's root page.
*
* When the Index Queue Worker task is executed by a cron job there is no
* HTTP_HOST since we are in a CLI environment. RealURL needs the host
* information to generate a proper URL though. Using the Index Queue item's
* root page information we can determine the correct host although being
* in a CLI environment.
*
* @param Item $item Index Queue item to use to determine the host.
* @param
*/
protected function initializeHttpServerEnvironment(Item $item)
{
static $hosts = [];
$rootpageId = $item->getRootPageUid();
$hostFound = !empty($hosts[$rootpageId]);
if (!$hostFound) {
$hosts[$rootpageId] = $item->getSite()->getDomain();
}
$_SERVER['HTTP_HOST'] = $hosts[$rootpageId];
// needed since TYPO3 7.5
GeneralUtility::flushInternalRuntimeCaches();
}
/**
* @param string|null $originalHttpHost
*/
protected function restoreOriginalHttpHost($originalHttpHost)
{
if (!is_null($originalHttpHost)) {
$_SERVER['HTTP_HOST'] = $originalHttpHost;
} else {
unset($_SERVER['HTTP_HOST']);
}
// needed since TYPO3 7.5
GeneralUtility::flushInternalRuntimeCaches();
}
}

View File

@@ -0,0 +1,159 @@
<?php declare(strict_types = 1);
namespace WapplerSystems\Meilisearch\Domain\Index\PageIndexer\Helper\UriBuilder;
/***************************************************************
* Copyright notice
*
* (c) 2019 Timo Hund <timo.hund@dkd.de>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project is
* free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
* A copy is found in the textfile GPL.txt and important notices to the license
* from the author is found in LICENSE.txt distributed with these scripts.
*
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use WapplerSystems\Meilisearch\IndexQueue\Item;
use WapplerSystems\Meilisearch\IndexQueue\PageIndexerDataUrlModifier;
use WapplerSystems\Meilisearch\System\Logging\SolrLogManager;
use WapplerSystems\Meilisearch\System\Url\UrlHelper;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Implementations of this class are able to build an indexing url for solr page indexing.
*/
abstract class AbstractUriStrategy
{
/**
* @var SolrLogManager|null|object
*/
protected $logger;
/**
* AbstractUriStrategy constructor.
* @param SolrLogManager|null $logger
*/
public function __construct(SolrLogManager $logger = null)
{
$this->logger = $logger ?? GeneralUtility::makeInstance(SolrLogManager::class, /** @scrutinizer ignore-type */ __CLASS__);
}
/**
* @param UrlHelper $urlHelper
* @param array $overrideConfiguration
* @return UrlHelper
*/
protected function applyTypoScriptOverridesOnIndexingUrl(UrlHelper $urlHelper, array $overrideConfiguration = []): UrlHelper
{
// check whether we should use ssl / https
if (!empty($overrideConfiguration['scheme'])) {
$urlHelper->setScheme($overrideConfiguration['scheme']);
}
// overwriting the host
if (!empty($overrideConfiguration['host'])) {
$urlHelper->setHost($overrideConfiguration['host']);
}
// overwriting the port
if (!empty($overrideConfiguration['port'])) {
$urlHelper->setPort($overrideConfiguration['port']);
}
// setting a path if TYPO3 is installed in a sub directory
if (!empty($overrideConfiguration['path'])) {
$urlHelper->setPath($overrideConfiguration['path']);
}
return $urlHelper;
}
/**
* @param Item $item
* @param int $language
* @param string $mountPointParameter
* @param array $options
* @return string
*/
public function getPageIndexingUriFromPageItemAndLanguageId(Item $item, int $language = 0, string $mountPointParameter = '', $options = []): string
{
$pageIndexUri = $this->buildPageIndexingUriFromPageItemAndLanguageId($item, $language, $mountPointParameter);
$urlHelper = GeneralUtility::makeInstance(UrlHelper::class, $pageIndexUri);
$overrideConfiguration = is_array($options['frontendDataHelper.']) ? $options['frontendDataHelper.'] : [];
$urlHelper = $this->applyTypoScriptOverridesOnIndexingUrl($urlHelper, $overrideConfiguration);
$dataUrl = $urlHelper->getUrl();
if (!GeneralUtility::isValidUrl($dataUrl)) {
$this->logger->log(
SolrLogManager::ERROR,
'Could not create a valid URL to get frontend data while trying to index a page.',
[
'item' => (array)$item,
'constructed URL' => $dataUrl,
'scheme' => $urlHelper->getScheme(),
'host' => $urlHelper->getHost(),
'path' => $urlHelper->getPath(),
'page ID' => $item->getRecordUid(),
'indexer options' => $options
]
);
throw new \RuntimeException(
'Could not create a valid URL to get frontend data while trying to index a page. Created URL: ' . $dataUrl,
1311080805
);
}
return $this->applyDataUrlModifier($item, $language, $dataUrl, $urlHelper);
}
/**
* @param Item $item
* @param int $language
* @param string $mountPointParameter
* @return mixed
*/
abstract protected function buildPageIndexingUriFromPageItemAndLanguageId(Item $item, int $language = 0, string $mountPointParameter = '');
/**
* @param Item $item
* @param int $language
* @param string $dataUrl
* @param UrlHelper $urlHelper
* @return string
*/
protected function applyDataUrlModifier(Item $item, int $language, $dataUrl, UrlHelper $urlHelper):string
{
if (empty($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr']['IndexQueuePageIndexer']['dataUrlModifier'])) {
return $dataUrl;
}
$dataUrlModifier = GeneralUtility::makeInstance($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr']['IndexQueuePageIndexer']['dataUrlModifier']);
if (!$dataUrlModifier instanceof PageIndexerDataUrlModifier) {
throw new \RuntimeException($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr']['IndexQueuePageIndexer']['dataUrlModifier'] . ' is not an implementation of WapplerSystems\Meilisearch\IndexQueue\PageIndexerDataUrlModifier', 1290523345);
}
return $dataUrlModifier->modifyDataUrl($dataUrl,
[
'item' => $item, 'scheme' => $urlHelper->getScheme(), 'host' => $urlHelper->getHost(),
'path' => $urlHelper->getPath(), 'pageId' => $item->getRecordUid(), 'language' => $language
]
);
}
}

View File

@@ -0,0 +1,85 @@
<?php declare(strict_types = 1);
namespace WapplerSystems\Meilisearch\Domain\Index\PageIndexer\Helper\UriBuilder;
/***************************************************************
* Copyright notice
*
* (c) 2019 Timo Hund <timo.hund@dkd.de>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project is
* free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
* A copy is found in the textfile GPL.txt and important notices to the license
* from the author is found in LICENSE.txt distributed with these scripts.
*
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use WapplerSystems\Meilisearch\IndexQueue\Item;
use WapplerSystems\Meilisearch\System\Logging\SolrLogManager;
use TYPO3\CMS\Core\Exception\SiteNotFoundException;
use TYPO3\CMS\Core\Routing\InvalidRouteArgumentsException;
use TYPO3\CMS\Core\Site\SiteFinder;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* This class is used to build the indexing url for a TYPO3 site that is managed with the TYPO3 site management.
*
* These sites have the pageId and language information encoded in the speaking url.
*/
class TYPO3SiteStrategy extends AbstractUriStrategy
{
/**
* @var SiteFinder
*/
protected $siteFinder = null;
/**
* TYPO3SiteStrategy constructor.
* @param SolrLogManager|null $logger
* @param SiteFinder|null $siteFinder
*/
public function __construct(SolrLogManager $logger = null, SiteFinder $siteFinder = null)
{
parent::__construct($logger);
$this->siteFinder = $siteFinder ?? GeneralUtility::makeInstance(SiteFinder::class);
}
/**
* @param Item $item
* @param int $language
* @param string $mountPointParameter
* @return string
* @throws SiteNotFoundException
* @throws InvalidRouteArgumentsException
*/
protected function buildPageIndexingUriFromPageItemAndLanguageId(Item $item, int $language = 0, string $mountPointParameter = '')
{
$site = $this->siteFinder->getSiteByPageId((int)$item->getRecordUid());
$parameters = [];
if ($language > 0) {
$parameters['_language'] = $language;
};
if ($mountPointParameter !== '') {
$parameters['MP'] = $mountPointParameter;
}
$pageIndexUri = (string)$site->getRouter()->generateUri($item->getRecord(), $parameters);
return $pageIndexUri;
}
}

View File

@@ -0,0 +1,61 @@
<?php declare(strict_types = 1);
namespace WapplerSystems\Meilisearch\Domain\Index\PageIndexer\Helper;
/***************************************************************
* Copyright notice
*
* (c) 2019 Timo Hund <timo.hund@dkd.de>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project is
* free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
* A copy is found in the textfile GPL.txt and important notices to the license
* from the author is found in LICENSE.txt distributed with these scripts.
*
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use WapplerSystems\Meilisearch\Domain\Index\PageIndexer\Helper\UriBuilder\AbstractUriStrategy;
use WapplerSystems\Meilisearch\Domain\Index\PageIndexer\Helper\UriBuilder\TYPO3SiteStrategy;
use WapplerSystems\Meilisearch\System\Util\SiteUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* This class is responsible to retrieve an "UriStrategy" the can build uri's for the site where the
* passed page belongs to.
*
* This can be:
* * A TYPO3 site managed with site management
* * A TYPO3 site without site management where the url is build by EXT:meilisearch with L and id param and information from the domain
* record or solr specific configuration.
*/
class UriStrategyFactory
{
/**
* @param integer $pageId
* @oaram array $overrideConfiguration
* @return AbstractUriStrategy
* @throws \Exception
*/
public function getForPageId(int $pageId): AbstractUriStrategy
{
if (!SiteUtility::getIsSiteManagedSite($pageId)) {
throw new \Exception('Site of page with uid ' . $pageId . ' is not a TYPO3 managed site');
}
return GeneralUtility::makeInstance(TYPO3SiteStrategy::class);
}
}

View File

@@ -0,0 +1,175 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Index\Queue\GarbageRemover;
/***************************************************************
* Copyright notice
*
* (c) 2018 - Timo Hund <timo.hund@dkd.de>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project is
* free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use WapplerSystems\Meilisearch\ConnectionManager;
use WapplerSystems\Meilisearch\GarbageCollectorPostProcessor;
use WapplerSystems\Meilisearch\IndexQueue\Queue;
use WapplerSystems\Meilisearch\System\Solr\SolrConnection;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* An implementation ob a garbage remover strategy is responsible to remove all garbage from the index queue and
* the solr server for a certain table and uid combination.
*/
abstract class AbstractStrategy
{
/**
* @var Queue
*/
protected $queue;
/**
* @var ConnectionManager
*/
protected $connectionManager;
/**
* AbstractStrategy constructor.
* @param Queue|null $queue
* @param ConnectionManager|null $connectionManager
*/
public function __construct(Queue $queue = null, ConnectionManager $connectionManager = null)
{
$this->queue = $queue ?? GeneralUtility::makeInstance(Queue::class);
$this->connectionManager = $connectionManager ?? GeneralUtility::makeInstance(ConnectionManager::class);
}
/**
* Call's the removal of the strategy and afterwards the garbagecollector post processing hook.
*
* @param string $table
* @param int $uid
* @return mixed
*/
public function removeGarbageOf($table, $uid)
{
$this->removeGarbageOfByStrategy($table, $uid);
$this->callPostProcessGarbageCollectorHook($table, $uid);
}
/**
* A implementation of the GarbageCollection strategy is responsible to remove the garbage from
* the indexqueue and from the solr server.
*
* @param string $table
* @param int $uid
* @return mixed
*/
abstract protected function removeGarbageOfByStrategy($table, $uid);
/**
* Deletes a document from solr and from the index queue.
*
* @param string $table
* @param integer $uid
*/
protected function deleteInSolrAndRemoveFromIndexQueue($table, $uid)
{
$this->deleteIndexDocuments($table, $uid);
$this->queue->deleteItem($table, $uid);
}
/**
* Deletes a document from solr and updates the item in the index queue (e.g. on page content updates).
*
* @param string $table
* @param integer $uid
*/
protected function deleteInSolrAndUpdateIndexQueue($table, $uid)
{
$this->deleteIndexDocuments($table, $uid);
$this->queue->updateItem($table, $uid);
}
/**
* Deletes index documents for a given record identification.
*
* @param string $table The record's table name.
* @param int $uid The record's uid.
*/
protected function deleteIndexDocuments($table, $uid, $language = 0)
{
// record can be indexed for multiple sites
$indexQueueItems = $this->queue->getItems($table, $uid);
foreach ($indexQueueItems as $indexQueueItem) {
$site = $indexQueueItem->getSite();
$enableCommitsSetting = $site->getSolrConfiguration()->getEnableCommits();
$siteHash = $site->getSiteHash();
// a site can have multiple connections (cores / languages)
$solrConnections = $this->connectionManager->getConnectionsBySite($site);
if ($language > 0) {
$solrConnections = [$language => $solrConnections[$language]];
}
$this->deleteRecordInAllSolrConnections($table, $uid, $solrConnections, $siteHash, $enableCommitsSetting);
}
}
/**
* Deletes the record in all solr connections from that site.
*
* @param string $table
* @param int $uid
* @param SolrConnection[] $solrConnections
* @param string $siteHash
* @param boolean $enableCommitsSetting
*/
protected function deleteRecordInAllSolrConnections($table, $uid, $solrConnections, $siteHash, $enableCommitsSetting)
{
foreach ($solrConnections as $solr) {
$solr->getWriteService()->deleteByQuery('type:' . $table . ' AND uid:' . (int)$uid . ' AND siteHash:' . $siteHash);
if ($enableCommitsSetting) {
$solr->getWriteService()->commit(false, false);
}
}
}
/**
* Calls the registered post processing hooks after the garbageCollection.
*
* @param string $table
* @param int $uid
*/
protected function callPostProcessGarbageCollectorHook($table, $uid)
{
if (!is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr']['postProcessGarbageCollector'])) {
return;
}
foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr']['postProcessGarbageCollector'] as $classReference) {
$garbageCollectorPostProcessor = GeneralUtility::makeInstance($classReference);
if ($garbageCollectorPostProcessor instanceof GarbageCollectorPostProcessor) {
$garbageCollectorPostProcessor->postProcessGarbageCollector($table, $uid);
} else {
$message = get_class($garbageCollectorPostProcessor) . ' must implement interface ' .
GarbageCollectorPostProcessor::class;
throw new \UnexpectedValueException($message, 1345807460);
}
}
}
}

View File

@@ -0,0 +1,80 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Index\Queue\GarbageRemover;
/***************************************************************
* Copyright notice
*
* (c) 2018 - Timo Hund <timo.hund@dkd.de>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project is
* free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use TYPO3\CMS\Backend\Utility\BackendUtility;
/**
* Class PageStrategy
*/
class PageStrategy extends AbstractStrategy {
/**
* @param string $table
* @param int $uid
* @return mixed
*/
protected function removeGarbageOfByStrategy($table, $uid)
{
if ($table === 'tt_content') {
$this->collectPageGarbageByContentChange($uid);
return;
}
if ($table === 'pages') {
$this->collectPageGarbageByPageChange($uid);
return;
}
}
/**
* Determines the relevant page id for an content element update. Deletes the page from solr and requeues the
* page for a reindex.
*
* @param int $ttContentUid
*/
protected function collectPageGarbageByContentChange($ttContentUid)
{
$contentElement = BackendUtility::getRecord('tt_content', $ttContentUid, 'uid, pid', '', false);
$this->deleteInSolrAndUpdateIndexQueue('pages', $contentElement['pid']);
}
/**
* When a page was changed it is removed from the index and index queue.
*
* @param int $uid
*/
protected function collectPageGarbageByPageChange($uid)
{
$pageOverlay = BackendUtility::getRecord('pages', $uid, 'l10n_parent, sys_language_uid', '', false);
if (!empty($pageOverlay['l10n_parent']) && intval($pageOverlay['l10n_parent']) !== 0) {
$this->deleteIndexDocuments('pages', (int)$pageOverlay['l10n_parent'], (int)$pageOverlay['sys_language_uid']);
} else {
$this->deleteInSolrAndRemoveFromIndexQueue('pages', $uid);
}
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Index\Queue\GarbageRemover;
/***************************************************************
* Copyright notice
*
* (c) 2018 - Timo Hund <timo.hund@dkd.de>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project is
* free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
/**
* Class RecordStrategy
*/
class RecordStrategy extends AbstractStrategy {
/**
* Removes the garbage of a record.
*
* @param string $table
* @param int $uid
* @return mixed
*/
protected function removeGarbageOfByStrategy($table, $uid)
{
$this->deleteInSolrAndRemoveFromIndexQueue($table, $uid);
}
}

View File

@@ -0,0 +1,42 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Index\Queue\GarbageRemover;
/***************************************************************
* Copyright notice
*
* (c) 2018 - Timo Hund <timo.hund@dkd.de>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project is
* free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
/**
* Class StrategyFactory
*/
class StrategyFactory {
/**
* @param string $table
* @return PageStrategy|RecordStrategy
*/
public static function getByTable($table)
{
$isPageRelated = in_array($table, ['tt_content','pages']);
return $isPageRelated ? new PageStrategy() : new RecordStrategy();
}
}

View File

@@ -0,0 +1,86 @@
<?php declare(strict_types = 1);
namespace WapplerSystems\Meilisearch\Domain\Index\Queue;
/***************************************************************
* Copyright notice
*
* (c) 2010-2017 dkd Internet Service GmbH <solr-support@dkd.de>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project is
* free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use WapplerSystems\Meilisearch\System\Records\AbstractRepository;
/**
* Class IndexQueueIndexingPropertyRepository
* Handles all CRUD operations to tx_meilisearch_indexqueue_indexing_property table
*/
class IndexQueueIndexingPropertyRepository extends AbstractRepository
{
/**
* @var string
*/
protected $table = 'tx_meilisearch_indexqueue_indexing_property';
/**
* Removes existing indexing properties.
*
* @param int $rootPid
* @param int $indexQueueUid
* @return int
*/
public function removeByRootPidAndIndexQueueUid(int $rootPid, int $indexQueueUid) : int
{
$queryBuider = $this->getQueryBuilder();
return $queryBuider
->delete($this->table)
->where(
$queryBuider->expr()->eq('root', $queryBuider->createNamedParameter($rootPid, \PDO::PARAM_INT)),
$queryBuider->expr()->eq('item_id', $queryBuider->createNamedParameter($indexQueueUid, \PDO::PARAM_INT))
)->execute();
}
/**
* Inserts a list of given properties
*
* @param array $properties assoc array with column names as key
* @return int
*/
public function bulkInsert(array $properties) : int
{
return $this->getQueryBuilder()->getConnection()->bulkInsert($this->table, $properties, ['root', 'item_id', 'property_key', 'property_value']);
}
/**
* Fetches a list of properties related to index queue item
*
* @param int $indexQueueUid
* @return array list of records for searched index queue item
*/
public function findAllByIndexQueueUid(int $indexQueueUid) : array
{
$queryBuider = $this->getQueryBuilder();
return $queryBuider
->select('property_key', 'property_value')
->from($this->table)
->where(
$queryBuider->expr()->eq('item_id', $queryBuider->createNamedParameter($indexQueueUid, \PDO::PARAM_INT))
)
->execute()->fetchAll();
}
}

View File

@@ -0,0 +1,162 @@
<?php declare(strict_types = 1);
namespace WapplerSystems\Meilisearch\Domain\Index\Queue;
/***************************************************************
* Copyright notice
*
* (c) 2010-2017 dkd Internet Service GmbH <solr-support@dkd.de>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project is
* free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use WapplerSystems\Meilisearch\IndexQueue\InitializationPostProcessor;
use WapplerSystems\Meilisearch\IndexQueue\Queue;
use WapplerSystems\Meilisearch\Domain\Site\Site;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* The queue initialization service is responsible to run the initialization of the index queue for a combination of sites
* and index queue configurations.
*
* @author Timo Hund <timo.hund@dkd.de>
* @author Ingo Renner <ingo.renner@dkd.de>
*/
class QueueInitializationService {
/**
* @var Queue
*/
protected $queue;
/**
* QueueInitializationService constructor.
*/
public function __construct(Queue $queue)
{
$this->queue = $queue;
}
/**
* Truncate and rebuild the tx_meilisearch_indexqueue_item table. This is the most
* complete way to force reindexing, or to build the Index Queue for the
* first time. The Index Queue initialization is site-specific.
*
* @param Site $site The site to initialize
* @param string $indexingConfigurationName Name of a specific indexing configuration, when * is passed any is used
* @return array An array of booleans, each representing whether the
* initialization for an indexing configuration was successful
*/
public function initializeBySiteAndIndexConfiguration(Site $site, $indexingConfigurationName = '*'): array
{
return $this->initializeBySiteAndIndexConfigurations($site, [$indexingConfigurationName]);
}
/**
* Truncates and rebuilds the tx_meilisearch_indexqueue_item table for a set of sites and a set of index configurations.
*
* @param array $sites The array of sites to initialize
* @param array $indexingConfigurationNames the array of index configurations to initialize.
* @return array
*/
public function initializeBySitesAndConfigurations(array $sites, array $indexingConfigurationNames = ['*']): array
{
$initializationStatesBySiteId = [];
foreach($sites as $site) {
/** @var Site $site */
$initializationResult = $this->initializeBySiteAndIndexConfigurations($site, $indexingConfigurationNames);
$initializationStatesBySiteId[$site->getRootPageId()] = $initializationResult;
}
return $initializationStatesBySiteId;
}
/**
* Initializes a set index configurations for a given site.
*
* @param Site $site
* @param array $indexingConfigurationNames if one of the names is a * (wildcard) all configurations are used,
* @return array
*/
public function initializeBySiteAndIndexConfigurations(Site $site, array $indexingConfigurationNames): array
{
$initializationStatus = [];
$hasWildcardConfiguration = in_array('*', $indexingConfigurationNames);
$indexingConfigurationNames = $hasWildcardConfiguration ? $site->getSolrConfiguration()->getEnabledIndexQueueConfigurationNames() : $indexingConfigurationNames;
foreach ($indexingConfigurationNames as $indexingConfigurationName) {
$initializationStatus[$indexingConfigurationName] = $this->applyInitialization($site, (string)$indexingConfigurationName);
}
if (!is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr']['postProcessIndexQueueInitialization'])) {
return $initializationStatus;
}
foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr']['postProcessIndexQueueInitialization'] as $classReference) {
$indexQueueInitializationPostProcessor = GeneralUtility::makeInstance($classReference);
if ($indexQueueInitializationPostProcessor instanceof InitializationPostProcessor) {
$indexQueueInitializationPostProcessor->postProcessIndexQueueInitialization($site, $indexingConfigurationNames, $initializationStatus);
} else {
throw new \UnexpectedValueException(get_class($indexQueueInitializationPostProcessor) . ' must implement interface ' . InitializationPostProcessor::class, 1345815561);
}
}
return $initializationStatus;
}
/**
* Initializes the Index Queue for a specific indexing configuration.
*
* @param Site $site The site to initialize
* @param string $indexingConfigurationName name of a specific
* indexing configuration
* @return bool TRUE if the initialization was successful, FALSE otherwise
*/
protected function applyInitialization(Site $site, $indexingConfigurationName): bool
{
// clear queue
$this->queue->deleteItemsBySite($site, $indexingConfigurationName);
$solrConfiguration = $site->getSolrConfiguration();
$tableToIndex = $solrConfiguration->getIndexQueueTableNameOrFallbackToConfigurationName($indexingConfigurationName);
$initializerClass = $solrConfiguration->getIndexQueueInitializerClassByConfigurationName($indexingConfigurationName);
$indexConfiguration = $solrConfiguration->getIndexQueueConfigurationByName($indexingConfigurationName);
return $this->executeInitializer($site, $indexingConfigurationName, $initializerClass, $tableToIndex, $indexConfiguration);
}
/**
* @param Site $site
* @param string $indexingConfigurationName
* @param string $initializerClass
* @param string $tableToIndex
* @param array $indexConfiguration
* @return bool
*/
protected function executeInitializer(Site $site, $indexingConfigurationName, $initializerClass, $tableToIndex, $indexConfiguration): bool
{
$initializer = GeneralUtility::makeInstance($initializerClass);
/** @var $initializer \WapplerSystems\Meilisearch\IndexQueue\Initializer\AbstractInitializer */
$initializer->setSite($site);
$initializer->setType($tableToIndex);
$initializer->setIndexingConfigurationName($indexingConfigurationName);
$initializer->setIndexingConfiguration($indexConfiguration);
return $initializer->initialize();
}
}

View File

@@ -0,0 +1,902 @@
<?php declare(strict_types = 1);
namespace WapplerSystems\Meilisearch\Domain\Index\Queue;
/***************************************************************
* Copyright notice
*
* (c) 2010-2017 dkd Internet Service GmbH <solr-support@dkd.de>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project is
* free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use WapplerSystems\Meilisearch\IndexQueue\Item;
use WapplerSystems\Meilisearch\Domain\Site\Site;
use WapplerSystems\Meilisearch\System\Logging\SolrLogManager;
use WapplerSystems\Meilisearch\System\Records\AbstractRepository;
use Doctrine\DBAL\DBALException;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\Expression\CompositeExpression;
use TYPO3\CMS\Core\Database\Query\QueryBuilder;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Class QueueItemRepository
* Handles all CRUD operations to tx_meilisearch_indexqueue_item table
*
*/
class QueueItemRepository extends AbstractRepository
{
/**
* @var string
*/
protected $table = 'tx_meilisearch_indexqueue_item';
/**
* @var SolrLogManager
*/
protected $logger;
/**
* QueueItemRepository constructor.
*
* @param SolrLogManager|null $logManager
*/
public function __construct(SolrLogManager $logManager = null)
{
$this->logger = $logManager ?? GeneralUtility::makeInstance(SolrLogManager::class, /** @scrutinizer ignore-type */ __CLASS__);
}
/**
* Fetches the last indexed row
*
* @param int $rootPageId The root page uid for which to get the last indexed row
* @return array
*/
public function findLastIndexedRow(int $rootPageId) : array
{
$queryBuilder = $this->getQueryBuilder();
$row = $queryBuilder
->select('uid', 'indexed')
->from($this->table)
->where($queryBuilder->expr()->eq('root', $rootPageId))
->andWhere($queryBuilder->expr()->neq('indexed', 0))
->orderBy('indexed', 'DESC')
->setMaxResults(1)
->execute()->fetchAll();
return $row;
}
/**
* Finds indexing errors for the current site
*
* @param Site $site
* @return array Error items for the current site's Index Queue
*/
public function findErrorsBySite(Site $site) : array
{
$queryBuilder = $this->getQueryBuilder();
$errors = $queryBuilder
->select('uid', 'item_type', 'item_uid', 'errors')
->from($this->table)
->andWhere(
$queryBuilder->expr()->notLike('errors', $queryBuilder->createNamedParameter('')),
$queryBuilder->expr()->eq('root', $site->getRootPageId())
)
->execute()->fetchAll();
return $errors;
}
/**
* Resets all the errors for all index queue items.
*
* @return int affected rows
*/
public function flushAllErrors() : int
{
$queryBuilder = $this->getQueryBuilder();
$affectedRows = $this->getPreparedFlushErrorQuery($queryBuilder)->execute();
return $affectedRows;
}
/**
* Flushes the errors for a single site.
*
* @param Site $site
* @return int
*/
public function flushErrorsBySite(Site $site) : int
{
$queryBuilder = $this->getQueryBuilder();
$affectedRows = $this->getPreparedFlushErrorQuery($queryBuilder)
->andWhere(
$queryBuilder->expr()->eq('root', (int)$site->getRootPageId())
)
->execute();
return $affectedRows;
}
/**
* Flushes the error for a single item.
*
* @param Item $item
* @return int affected rows
*/
public function flushErrorByItem(Item $item) : int
{
$queryBuilder = $this->getQueryBuilder();
$affectedRows = $this->getPreparedFlushErrorQuery($queryBuilder)
->andWhere(
$queryBuilder->expr()->eq('uid', $item->getIndexQueueUid())
)
->execute();
return $affectedRows;
}
/**
* Initializes the QueryBuilder with a query the resets the error field for items that have an error.
*
* @return QueryBuilder
*/
private function getPreparedFlushErrorQuery(QueryBuilder $queryBuilder)
{
return $queryBuilder
->update($this->table)
->set('errors', '')
->where(
$queryBuilder->expr()->notLike('errors', $queryBuilder->createNamedParameter(''))
);
}
/**
* Updates an existing queue entry by $itemType $itemUid and $rootPageId.
*
* @param string $itemType The item's type, usually a table name.
* @param int $itemUid The item's uid, usually an integer uid, could be a
* different value for non-database-record types.
* @param string $indexingConfiguration The name of the related indexConfiguration
* @param int $rootPageId The uid of the rootPage
* @param int $changedTime The forced change time that should be used for updating
* @return int affected rows
*/
public function updateExistingItemByItemTypeAndItemUidAndRootPageId(string $itemType, int $itemUid, int $rootPageId, int $changedTime, string $indexingConfiguration = '') : int
{
$queryBuilder = $this->getQueryBuilder();
$queryBuilder
->update($this->table)
->set('changed', $changedTime)
->andWhere(
$queryBuilder->expr()->eq('item_type', $queryBuilder->createNamedParameter($itemType)),
$queryBuilder->expr()->eq('item_uid', $itemUid),
$queryBuilder->expr()->eq('root', $rootPageId)
);
if (!empty($indexingConfiguration)) {
$queryBuilder->set('indexing_configuration', $indexingConfiguration);
}
return $queryBuilder->execute();
}
/**
* Adds an item to the index queue.
*
* Not meant for public use.
*
* @param string $itemType The item's type, usually a table name.
* @param int $itemUid The item's uid, usually an integer uid, could be a different value for non-database-record types.
* @param int $rootPageId
* @param int $changedTime
* @param string $indexingConfiguration The item's indexing configuration to use. Optional, overwrites existing / determined configuration.
* @return int the number of inserted rows, which is typically 1
*/
public function add(string $itemType, int $itemUid, int $rootPageId, int $changedTime, string $indexingConfiguration) : int
{
$queryBuilder = $this->getQueryBuilder();
return $queryBuilder
->insert($this->table)
->values([
'root' => $rootPageId,
'item_type' => $itemType,
'item_uid' => $itemUid,
'changed' => $changedTime,
'errors' => '',
'indexing_configuration' => $indexingConfiguration
])
->execute();
}
/**
* Retrieves the count of items that match certain filters. Each filter is passed as parts of the where claus combined with AND.
*
* @param array $sites
* @param array $indexQueueConfigurationNames
* @param array $itemTypes
* @param array $itemUids
* @param array $uids
* @return int
*/
public function countItems(array $sites = [], array $indexQueueConfigurationNames = [], array $itemTypes = [], array $itemUids = [], array $uids = []): int
{
$rootPageIds = Site::getRootPageIdsFromSites($sites);
$indexQueueConfigurationList = implode(",", $indexQueueConfigurationNames);
$itemTypeList = implode(",", $itemTypes);
$itemUids = array_map("intval", $itemUids);
$uids = array_map("intval", $uids);
$queryBuilderForCountingItems = $this->getQueryBuilder();
$queryBuilderForCountingItems->count('uid')->from($this->table);
$queryBuilderForCountingItems = $this->addItemWhereClauses($queryBuilderForCountingItems, $rootPageIds, $indexQueueConfigurationList, $itemTypeList, $itemUids, $uids);
return (int)$queryBuilderForCountingItems->execute()->fetchColumn(0);
}
/**
* Gets the most recent changed time of a page's content elements
*
* @param int $pageUid
* @return int|null Timestamp of the most recent content element change or null if nothing is found.
*/
public function getPageItemChangedTimeByPageUid(int $pageUid)
{
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('tt_content');
$queryBuilder->getRestrictions()->removeAll();
$pageContentLastChangedTime = $queryBuilder
->add('select', $queryBuilder->expr()->max('tstamp', 'changed_time'))
->from('tt_content')
->where(
$queryBuilder->expr()->eq('pid', $pageUid)
)
->execute()->fetch();
return $pageContentLastChangedTime['changed_time'];
}
/**
* Gets the most recent changed time for an item taking into account
* localized records.
*
* @param string $itemType The item's type, usually a table name.
* @param int $itemUid The item's uid
* @return int Timestamp of the most recent content element change
*/
public function getLocalizableItemChangedTime(string $itemType, int $itemUid) : int
{
$localizedChangedTime = 0;
if (isset($GLOBALS['TCA'][$itemType]['ctrl']['transOrigPointerField'])) {
// table is localizable
$translationOriginalPointerField = $GLOBALS['TCA'][$itemType]['ctrl']['transOrigPointerField'];
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($itemType);
$queryBuilder->getRestrictions()->removeAll();
$localizedChangedTime = $queryBuilder
->add('select', $queryBuilder->expr()->max('tstamp', 'changed_time'))
->from($itemType)
->orWhere(
$queryBuilder->expr()->eq('uid', $itemUid),
$queryBuilder->expr()->eq($translationOriginalPointerField, $itemUid)
)->execute()->fetchColumn(0);
}
return (int)$localizedChangedTime;
}
/**
* Returns prepared QueryBuilder for contains* methods in this repository
*
* @param string $itemType
* @param int $itemUid
* @return QueryBuilder
*/
protected function getQueryBuilderForContainsMethods(string $itemType, int $itemUid) : QueryBuilder
{
$queryBuilder = $this->getQueryBuilder();
return $queryBuilder->count('uid')->from($this->table)
->andWhere(
$queryBuilder->expr()->eq('item_type', $queryBuilder->createNamedParameter($itemType)),
$queryBuilder->expr()->eq('item_uid', $itemUid)
);
}
/**
* Checks whether the Index Queue contains a specific item.
*
* @param string $itemType The item's type, usually a table name.
* @param int $itemUid The item's uid
* @return bool TRUE if the item is found in the queue, FALSE otherwise
*/
public function containsItem(string $itemType, int $itemUid) : bool
{
return (bool)$this->getQueryBuilderForContainsMethods($itemType, $itemUid)->execute()->fetchColumn(0);
}
/**
* Checks whether the Index Queue contains a specific item.
*
* @param string $itemType The item's type, usually a table name.
* @param int $itemUid The item's uid
* @param integer $rootPageId
* @return bool TRUE if the item is found in the queue, FALSE otherwise
*/
public function containsItemWithRootPageId(string $itemType, int $itemUid, int $rootPageId) : bool
{
$queryBuilder = $this->getQueryBuilderForContainsMethods($itemType, $itemUid);
return (bool)$queryBuilder
->andWhere($queryBuilder->expr()->eq('root', $rootPageId))
->execute()->fetchColumn(0);
}
/**
* Checks whether the Index Queue contains a specific item that has been
* marked as indexed.
*
* @param string $itemType The item's type, usually a table name.
* @param int $itemUid The item's uid
* @return bool TRUE if the item is found in the queue and marked as
* indexed, FALSE otherwise
*/
public function containsIndexedItem(string $itemType, int $itemUid) : bool
{
$queryBuilder = $this->getQueryBuilderForContainsMethods($itemType, $itemUid);
return (bool)$queryBuilder
->andWhere($queryBuilder->expr()->gt('indexed', 0))
->execute()->fetchColumn(0);
}
/**
* Removes an item from the Index Queue.
*
* @param string $itemType The type of the item to remove, usually a table name.
* @param int $itemUid The uid of the item to remove
*/
public function deleteItem(string $itemType, int $itemUid = null)
{
$itemUids = empty($itemUid) ? [] : [$itemUid];
$this->deleteItems([], [], [$itemType], $itemUids);
}
/**
* Removes all items of a certain type from the Index Queue.
*
* @param string $itemType The type of items to remove, usually a table name.
*/
public function deleteItemsByType(string $itemType)
{
$this->deleteItem($itemType);
}
/**
* Removes all items of a certain site from the Index Queue. Accepts an
* optional parameter to limit the deleted items by indexing configuration.
*
* @param Site $site The site to remove items for.
* @param string $indexingConfigurationName Name of a specific indexing configuration
* @throws \Exception
*/
public function deleteItemsBySite(Site $site, string $indexingConfigurationName = '')
{
$indexingConfigurationNames = empty($indexingConfigurationName) ? [] : [$indexingConfigurationName];
$this->deleteItems([$site], $indexingConfigurationNames);
}
/**
* Removes items in the index queue filtered by the passed arguments.
*
* @param array $sites
* @param array $indexQueueConfigurationNames
* @param array $itemTypes
* @param array $itemUids
* @param array $uids
* @throws \Exception
*/
public function deleteItems(array $sites = [], array $indexQueueConfigurationNames = [], array $itemTypes = [], array $itemUids = [], array $uids = [])
{
$rootPageIds = Site::getRootPageIdsFromSites($sites);
$indexQueueConfigurationList = implode(",", $indexQueueConfigurationNames);
$itemTypeList = implode(",", $itemTypes);
$itemUids = array_map("intval", $itemUids);
$uids = array_map("intval", $uids);
$queryBuilderForDeletingItems = $this->getQueryBuilder();
$queryBuilderForDeletingItems->delete($this->table);
$queryBuilderForDeletingItems = $this->addItemWhereClauses($queryBuilderForDeletingItems, $rootPageIds, $indexQueueConfigurationList, $itemTypeList, $itemUids, $uids);
$queryBuilderForDeletingProperties = $this->buildQueryForPropertyDeletion($queryBuilderForDeletingItems, $rootPageIds, $indexQueueConfigurationList, $itemTypeList, $itemUids, $uids);
$queryBuilderForDeletingItems->getConnection()->beginTransaction();
try {
$queryBuilderForDeletingItems->execute();
$queryBuilderForDeletingProperties->execute();
$queryBuilderForDeletingItems->getConnection()->commit();
} catch (\Exception $e) {
$queryBuilderForDeletingItems->getConnection()->rollback();
throw $e;
}
}
/**
* Initializes the query builder to delete items in the index queue filtered by the passed arguments.
*
* @param array $rootPageIds filter on a set of rootPageUids.
* @param string $indexQueueConfigurationList
* @param string $itemTypeList
* @param array $itemUids filter on a set of item uids
* @param array $uids filter on a set of queue item uids
* @return QueryBuilder
*/
private function addItemWhereClauses(QueryBuilder $queryBuilderForDeletingItems, array $rootPageIds, string $indexQueueConfigurationList, string $itemTypeList, array $itemUids, array $uids): QueryBuilder
{
if (!empty($rootPageIds)) {
$queryBuilderForDeletingItems->andWhere($queryBuilderForDeletingItems->expr()->in('root', $rootPageIds));
};
if (!empty($indexQueueConfigurationList)) {
$queryBuilderForDeletingItems->andWhere($queryBuilderForDeletingItems->expr()->in('indexing_configuration', $queryBuilderForDeletingItems->createNamedParameter($indexQueueConfigurationList)));
}
if (!empty($itemTypeList)) {
$queryBuilderForDeletingItems->andWhere($queryBuilderForDeletingItems->expr()->in('item_type', $queryBuilderForDeletingItems->createNamedParameter($itemTypeList)));
}
if (!empty($itemUids)) {
$queryBuilderForDeletingItems->andWhere($queryBuilderForDeletingItems->expr()->in('item_uid', $itemUids));
}
if (!empty($uids)) {
$queryBuilderForDeletingItems->andWhere($queryBuilderForDeletingItems->expr()->in('uid', $uids));
}
return $queryBuilderForDeletingItems;
}
/**
* Initializes a query builder to delete the indexing properties of an item by the passed conditions.
*
* @param QueryBuilder $queryBuilderForDeletingItems
* @param array $rootPageIds
* @param string $indexQueueConfigurationList
* @param string $itemTypeList
* @param array $itemUids
* @param array $uids
* @return QueryBuilder
*/
private function buildQueryForPropertyDeletion(QueryBuilder $queryBuilderForDeletingItems, array $rootPageIds, string $indexQueueConfigurationList, string $itemTypeList, array $itemUids, array $uids): QueryBuilder
{
$queryBuilderForSelectingProperties = $queryBuilderForDeletingItems->getConnection()->createQueryBuilder();
$queryBuilderForSelectingProperties->select('items.uid')->from('tx_meilisearch_indexqueue_indexing_property', 'properties')->innerJoin(
'properties',
$this->table,
'items',
(string)$queryBuilderForSelectingProperties->expr()->andX(
$queryBuilderForSelectingProperties->expr()->eq('items.uid', $queryBuilderForSelectingProperties->quoteIdentifier('properties.item_id')),
empty($rootPageIds) ? '' : $queryBuilderForSelectingProperties->expr()->in('items.root', $rootPageIds),
empty($indexQueueConfigurationList) ? '' : $queryBuilderForSelectingProperties->expr()->in('items.indexing_configuration', $queryBuilderForSelectingProperties->createNamedParameter($indexQueueConfigurationList)),
empty($itemTypeList) ? '' : $queryBuilderForSelectingProperties->expr()->in('items.item_type', $queryBuilderForSelectingProperties->createNamedParameter($itemTypeList)),
empty($itemUids) ? '' : $queryBuilderForSelectingProperties->expr()->in('items.item_uid', $itemUids),
empty($uids) ? '' : $queryBuilderForSelectingProperties->expr()->in('items.uid', $uids)
)
);
$propertyEntriesToDelete = implode(',', array_column($queryBuilderForSelectingProperties->execute()->fetchAll(), 'uid'));
$queryBuilderForDeletingProperties = $queryBuilderForDeletingItems->getConnection()->createQueryBuilder();
// make sure executing the propety deletion query doesn't fail if there are no properties to delete
if (empty($propertyEntriesToDelete)) {
$propertyEntriesToDelete = '0';
}
$queryBuilderForDeletingProperties->delete('tx_meilisearch_indexqueue_indexing_property')->where(
$queryBuilderForDeletingProperties->expr()->in('item_id', $propertyEntriesToDelete)
);
return $queryBuilderForDeletingProperties;
}
/**
* Removes all items from the Index Queue.
*
* @return int The number of affected rows. For a truncate this is unreliable as theres no meaningful information.
*/
public function deleteAllItems()
{
return $this->getQueryBuilder()->getConnection()->truncate($this->table);
}
/**
* Gets a single Index Queue item by its uid.
*
* @param int $uid Index Queue item uid
* @return Item|null The request Index Queue item or NULL if no item with $itemId was found
*/
public function findItemByUid(int $uid)
{
$queryBuilder = $this->getQueryBuilder();
$indexQueueItemRecord = $queryBuilder
->select('*')
->from($this->table)
->where($queryBuilder->expr()->eq('uid', $uid))
->execute()->fetch();
if (!isset($indexQueueItemRecord['uid'])) {
return null;
}
/** @var Item $item*/
$item = GeneralUtility::makeInstance(Item::class, /** @scrutinizer ignore-type */ $indexQueueItemRecord);
return $item;
}
/**
* Gets Index Queue items by type and uid.
*
* @param string $itemType item type, usually the table name
* @param int $itemUid item uid
* @return Item[] An array of items matching $itemType and $itemUid
*/
public function findItemsByItemTypeAndItemUid(string $itemType, int $itemUid) : array
{
$queryBuilder = $this->getQueryBuilder();
$compositeExpression = $queryBuilder->expr()->andX(
$queryBuilder->expr()->eq('item_type', $queryBuilder->getConnection()->quote($itemType, \PDO::PARAM_STR)),
$queryBuilder->expr()->eq('item_uid', $itemUid)
);
return $this->getItemsByCompositeExpression($compositeExpression, $queryBuilder);
}
/**
* Returns a collection of items by CompositeExpression.
* D
*
* @param CompositeExpression|null $expression Optional expression to filter records.
* @param QueryBuilder|null $queryBuilder QueryBuilder to use
* @return array
*/
protected function getItemsByCompositeExpression(CompositeExpression $expression = null, QueryBuilder $queryBuilder = null) : array
{
if (!$queryBuilder instanceof QueryBuilder) {
$queryBuilder = $this->getQueryBuilder();
}
$queryBuilder->select('*')->from($this->table);
if (isset($expression)) {
$queryBuilder->where($expression);
}
$indexQueueItemRecords = $queryBuilder->execute()->fetchAll();
return $this->getIndexQueueItemObjectsFromRecords($indexQueueItemRecords);
}
/**
* Returns all items in the queue.
*
* @return Item[] all Items from Queue without restrictions
*/
public function findAll() : array
{
$queryBuilder = $this->getQueryBuilder();
$allRecords = $queryBuilder
->select('*')
->from($this->table)
->execute()->fetchAll();
return $this->getIndexQueueItemObjectsFromRecords($allRecords);
}
/**
* Gets $limit number of items to index for a particular $site.
*
* @param Site $site TYPO3 site
* @param int $limit Number of items to get from the queue
* @return Item[] Items to index to the given solr server
*/
public function findItemsToIndex(Site $site, int $limit = 50) : array
{
$queryBuilder = $this->getQueryBuilder();
// determine which items to index with this run
$indexQueueItemRecords = $queryBuilder
->select('*')
->from($this->table)
->andWhere(
$queryBuilder->expr()->eq('root', $site->getRootPageId()),
$queryBuilder->expr()->gt('changed', 'indexed'),
$queryBuilder->expr()->lte('changed', time()),
$queryBuilder->expr()->eq('errors', $queryBuilder->createNamedParameter(''))
)
->orderBy('indexing_priority', 'DESC')
->addOrderBy('changed', 'DESC')
->addOrderBy('uid', 'DESC')
->setMaxResults($limit)
->execute()->fetchAll();
return $this->getIndexQueueItemObjectsFromRecords($indexQueueItemRecords);
}
/**
* Retrieves the count of items that match certain filters. Each filter is passed as parts of the where claus combined with AND.
*
* @param array $sites
* @param array $indexQueueConfigurationNames
* @param array $itemTypes
* @param array $itemUids
* @param array $uids
* @param int $start
* @param int $limit
* @return array
*/
public function findItems(array $sites = [], array $indexQueueConfigurationNames = [], array $itemTypes = [], array $itemUids = [], array $uids = [], $start = 0, $limit = 50): array
{
$rootPageIds = Site::getRootPageIdsFromSites($sites);
$indexQueueConfigurationList = implode(",", $indexQueueConfigurationNames);
$itemTypeList = implode(",", $itemTypes);
$itemUids = array_map("intval", $itemUids);
$uids = array_map("intval", $uids);
$itemQueryBuilder = $this->getQueryBuilder()->select('*')->from($this->table);
$itemQueryBuilder = $this->addItemWhereClauses($itemQueryBuilder, $rootPageIds, $indexQueueConfigurationList, $itemTypeList, $itemUids, $uids);
$itemRecords = $itemQueryBuilder->setFirstResult($start)->setMaxResults($limit)->execute()->fetchAll();
return $this->getIndexQueueItemObjectsFromRecords($itemRecords);
}
/**
* Creates an array of WapplerSystems\Meilisearch\IndexQueue\Item objects from an array of
* index queue records.
*
* @param array $indexQueueItemRecords Array of plain index queue records
* @return array Array of WapplerSystems\Meilisearch\IndexQueue\Item objects
*/
protected function getIndexQueueItemObjectsFromRecords(array $indexQueueItemRecords) : array
{
$tableRecords = $this->getAllQueueItemRecordsByUidsGroupedByTable($indexQueueItemRecords);
return $this->getQueueItemObjectsByRecords($indexQueueItemRecords, $tableRecords);
}
/**
* Returns the records for suitable item type.
*
* @param array $indexQueueItemRecords
* @return array
*/
protected function getAllQueueItemRecordsByUidsGroupedByTable(array $indexQueueItemRecords) : array
{
$tableUids = [];
$tableRecords = [];
// grouping records by table
foreach ($indexQueueItemRecords as $indexQueueItemRecord) {
$tableUids[$indexQueueItemRecord['item_type']][] = $indexQueueItemRecord['item_uid'];
}
// fetching records by table, saves us a lot of single queries
foreach ($tableUids as $table => $uids) {
$uidList = implode(',', $uids);
$queryBuilderForRecordTable = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
$queryBuilderForRecordTable->getRestrictions()->removeAll();
$resultsFromRecordTable = $queryBuilderForRecordTable
->select('*')
->from($table)
->where($queryBuilderForRecordTable->expr()->in('uid', $uidList))
->execute();
$records = [];
while ($record = $resultsFromRecordTable->fetch()) {
$records[$record['uid']] = $record;
}
$tableRecords[$table] = $records;
$this->hookPostProcessFetchRecordsForIndexQueueItem($table, $uids, $tableRecords);
}
return $tableRecords;
}
/**
* Calls defined in postProcessFetchRecordsForIndexQueueItem hook method.
*
* @param string $table
* @param array $uids
* @param array $tableRecords
*
* @return void
*/
protected function hookPostProcessFetchRecordsForIndexQueueItem(string $table, array $uids, array &$tableRecords)
{
if (!is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr']['postProcessFetchRecordsForIndexQueueItem'])) {
return;
}
$params = ['table' => $table, 'uids' => $uids, 'tableRecords' => &$tableRecords];
foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr']['postProcessFetchRecordsForIndexQueueItem'] as $reference) {
GeneralUtility::callUserFunction($reference, $params, $this);
}
}
/**
* Instantiates a list of Item objects from database records.
*
* @param array $indexQueueItemRecords records from database
* @param array $tableRecords
* @return array
*/
protected function getQueueItemObjectsByRecords(array $indexQueueItemRecords, array $tableRecords) : array
{
$indexQueueItems = [];
foreach ($indexQueueItemRecords as $indexQueueItemRecord) {
if (isset($tableRecords[$indexQueueItemRecord['item_type']][$indexQueueItemRecord['item_uid']])) {
$indexQueueItems[] = GeneralUtility::makeInstance(
Item::class,
/** @scrutinizer ignore-type */ $indexQueueItemRecord,
/** @scrutinizer ignore-type */ $tableRecords[$indexQueueItemRecord['item_type']][$indexQueueItemRecord['item_uid']]
);
} else {
$this->logger->log(
SolrLogManager::ERROR,
'Record missing for Index Queue item. Item removed.',
[
$indexQueueItemRecord
]
);
$this->deleteItem($indexQueueItemRecord['item_type'],
$indexQueueItemRecord['item_uid']);
}
}
return $indexQueueItems;
}
/**
* Marks an item as failed and causes the indexer to skip the item in the
* next run.
*
* @param int|Item $item Either the item's Index Queue uid or the complete item
* @param string $errorMessage Error message
* @return int affected rows
*/
public function markItemAsFailed($item, string $errorMessage = ''): int
{
$itemUid = ($item instanceof Item) ? $item->getIndexQueueUid() : (int)$item;
$errorMessage = empty($errorMessage) ? '1' : $errorMessage;
$queryBuilder = $this->getQueryBuilder();
return (int)$queryBuilder
->update($this->table)
->set('errors', $errorMessage)
->where($queryBuilder->expr()->eq('uid', $itemUid))
->execute();
}
/**
* Sets the timestamp of when an item last has been indexed.
*
* @param Item $item
* @return int affected rows
*/
public function updateIndexTimeByItem(Item $item) : int
{
$queryBuilder = $this->getQueryBuilder();
return (int)$queryBuilder
->update($this->table)
->set('indexed', time())
->where($queryBuilder->expr()->eq('uid', $item->getIndexQueueUid()))
->execute();
}
/**
* Sets the change timestamp of an item.
*
* @param Item $item
* @param int $changedTime
* @return int affected rows
*/
public function updateChangedTimeByItem(Item $item, int $changedTime) : int
{
$queryBuilder = $this->getQueryBuilder();
return (int)$queryBuilder
->update($this->table)
->set('changed', $changedTime)
->where($queryBuilder->expr()->eq('uid', $item->getIndexQueueUid()))
->execute();
}
/**
* Initializes Queue by given sql
*
* Note: Do not use platform specific functions!
*
* @param string $sqlStatement Native SQL statement
* @return int The number of affected rows.
* @internal
* @throws DBALException
*/
public function initializeByNativeSQLStatement(string $sqlStatement) : int
{
return $this->getQueryBuilder()->getConnection()->exec($sqlStatement);
}
/**
* Retrieves an array of pageIds from mountPoints that allready have a queue entry.
*
* @param string $identifier identifier of the mount point
* @return array pageIds from mountPoints that allready have a queue entry
*/
public function findPageIdsOfExistingMountPagesByMountIdentifier(string $identifier) : array
{
$queryBuilder = $this->getQueryBuilder();
$resultSet = $queryBuilder
->select('item_uid')
->add('select', $queryBuilder->expr()->count('*', 'queueItemCount'), true)
->from($this->table)
->where(
$queryBuilder->expr()->eq('item_type', $queryBuilder->createNamedParameter('pages')),
$queryBuilder->expr()->eq('pages_mountidentifier', $queryBuilder->createNamedParameter($identifier))
)
->groupBy('item_uid')
->execute();
$mountedPagesIdsWithQueueItems = [];
while ($record = $resultSet->fetch()) {
if ($record['queueItemCount'] > 0) {
$mountedPagesIdsWithQueueItems[] = $record['item_uid'];
}
}
return $mountedPagesIdsWithQueueItems;
}
/**
* Retrieves an array of items for mount destinations mathed by root page ID, Mount Identifier and a list of mounted page IDs.
*
* @param int $rootPid
* @param string $identifier identifier of the mount point
* @param array $mountedPids An array of mounted page IDs
* @return array
*/
public function findAllIndexQueueItemsByRootPidAndMountIdentifierAndMountedPids(int $rootPid, string $identifier, array $mountedPids) : array
{
$queryBuilder = $this->getQueryBuilder();
return $queryBuilder
->select('*')
->from($this->table)
->where(
$queryBuilder->expr()->eq('root', $queryBuilder->createNamedParameter($rootPid, \PDO::PARAM_INT)),
$queryBuilder->expr()->eq('item_type', $queryBuilder->createNamedParameter('pages')),
$queryBuilder->expr()->in('item_uid', $mountedPids),
$queryBuilder->expr()->eq('has_indexing_properties', $queryBuilder->createNamedParameter(1, \PDO::PARAM_INT)),
$queryBuilder->expr()->eq('pages_mountidentifier', $queryBuilder->createNamedParameter($identifier))
)
->execute()->fetchAll();
}
/**
* Updates has_indexing_properties field for given Item
*
* @param int $itemUid
* @param bool $hasIndexingPropertiesFlag
* @return int number of affected rows, 1 on success
*/
public function updateHasIndexingPropertiesFlagByItemUid(int $itemUid, bool $hasIndexingPropertiesFlag): int
{
$queryBuilder = $this->getQueryBuilder();
return $queryBuilder
->update($this->table)
->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($itemUid, \PDO::PARAM_INT)))
->set('has_indexing_properties', $queryBuilder->createNamedParameter($hasIndexingPropertiesFlag, \PDO::PARAM_INT), false)
->execute();
}
}

View File

@@ -0,0 +1,186 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Index\Queue\RecordMonitor\Helper;
/***************************************************************
* Copyright notice
*
* (c) 2017 - Thomas Hohn <tho@systime.dk>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project is
* free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use WapplerSystems\Meilisearch\System\Cache\TwoLevelCache;
use WapplerSystems\Meilisearch\System\Configuration\TypoScriptConfiguration;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Extracted logic from the AbstractDataHandlerListener in order to
* handle ConfigurationAwareRecords
*
* @author Thomas Hohn Hund <tho@systime.dk>
*/
class ConfigurationAwareRecordService
{
/**
* Retrieves the name of the Index Queue Configuration for a record.
*
* @param string $recordTable Table to read from
* @param int $recordUid Id of the record
* @param TypoScriptConfiguration $solrConfiguration
* @return string|null Name of indexing configuration
*/
public function getIndexingConfigurationName($recordTable, $recordUid, TypoScriptConfiguration $solrConfiguration)
{
$name = null;
$indexingConfigurations = $solrConfiguration->getEnabledIndexQueueConfigurationNames();
foreach ($indexingConfigurations as $indexingConfigurationName) {
if (!$solrConfiguration->getIndexQueueConfigurationIsEnabled($indexingConfigurationName)) {
// ignore disabled indexing configurations
continue;
}
$record = $this->getRecordIfIndexConfigurationIsValid($recordTable, $recordUid,
$indexingConfigurationName, $solrConfiguration);
if (!empty($record)) {
$name = $indexingConfigurationName;
// FIXME currently returns after the first configuration match
break;
}
}
return $name;
}
/**
* Retrieves a record, taking into account the additionalWhereClauses of the
* Indexing Queue configurations.
*
* @param string $recordTable Table to read from
* @param int $recordUid Id of the record
* @param TypoScriptConfiguration $solrConfiguration
* @return array Record if found, otherwise empty array
*/
public function getRecord($recordTable, $recordUid, TypoScriptConfiguration $solrConfiguration)
{
$record = [];
$indexingConfigurations = $solrConfiguration->getEnabledIndexQueueConfigurationNames();
foreach ($indexingConfigurations as $indexingConfigurationName) {
$record = $this->getRecordIfIndexConfigurationIsValid($recordTable, $recordUid,
$indexingConfigurationName, $solrConfiguration);
if (!empty($record)) {
// if we found a record which matches the conditions, we can continue
break;
}
}
return $record;
}
/**
* This method return the record array if the table is valid for this indexingConfiguration.
* Otherwise an empty array will be returned.
*
* @param string $recordTable
* @param integer $recordUid
* @param string $indexingConfigurationName
* @param TypoScriptConfiguration $solrConfiguration
* @return array
*/
protected function getRecordIfIndexConfigurationIsValid($recordTable, $recordUid, $indexingConfigurationName, TypoScriptConfiguration $solrConfiguration)
{
if (!$this->isValidTableForIndexConfigurationName($recordTable, $indexingConfigurationName, $solrConfiguration)) {
return [];
}
$recordWhereClause = $solrConfiguration->getIndexQueueAdditionalWhereClauseByConfigurationName($indexingConfigurationName);
$row = $this->getRecordForIndexConfigurationIsValid($recordTable, $recordUid, $recordWhereClause);
return $row;
}
/**
* Returns the row need by getRecordIfIndexConfigurationIsValid either directly from database
* or from cache
*
* @param string $recordTable
* @param integer $recordUid
* @param string $recordWhereClause
*
* @return array
*/
protected function getRecordForIndexConfigurationIsValid($recordTable, $recordUid, $recordWhereClause)
{
$cache = GeneralUtility::makeInstance(TwoLevelCache::class, /** @scrutinizer ignore-type */ 'cache_runtime');
$cacheId = md5('ConfigurationAwareRecordService' . ':' . 'getRecordIfIndexConfigurationIsValid' . ':' . $recordTable . ':' . $recordUid . ':' . $recordWhereClause);
$row = $cache->get($cacheId);
if (!empty($row)) {
return $row;
}
$row = (array)BackendUtility::getRecord($recordTable, $recordUid, '*', $recordWhereClause);
$cache->set($cacheId, $row);
return $row;
}
/**
* This method is used to check if a table is an allowed table for an index configuration.
*
* @param string $recordTable
* @param string $indexingConfigurationName
* @param TypoScriptConfiguration $solrConfiguration
* @return boolean
*/
protected function isValidTableForIndexConfigurationName($recordTable, $indexingConfigurationName, TypoScriptConfiguration $solrConfiguration)
{
$tableToIndex = $solrConfiguration->getIndexQueueTableNameOrFallbackToConfigurationName($indexingConfigurationName);
$isMatchingTable = ($tableToIndex === $recordTable);
if ($isMatchingTable) {
return true;
}
return false;
}
/**
* This method retrieves the parent pages record when the parent record is accessible
* through the recordWhereClause
*
* @param int $recordUid
* @param string $parentWhereClause
* @return array
*/
protected function getPageOverlayRecordIfParentIsAccessible($recordUid, $parentWhereClause)
{
$overlayRecord = (array)BackendUtility::getRecord('pages', $recordUid, '*');
$overlayParentId = $overlayRecord['l10n_parent'];
$pageRecord = (array)BackendUtility::getRecord('pages', $overlayParentId, '*', $parentWhereClause);
if (empty($pageRecord)) {
return [];
}
return $overlayRecord;
}
}

View File

@@ -0,0 +1,112 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Index\Queue\RecordMonitor\Helper;
/***************************************************************
* Copyright notice
*
* (c) 2015-2016 Timo Hund <timo.hund@dkd.de>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project is
* free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use WapplerSystems\Meilisearch\Domain\Site\SiteRepository;
use WapplerSystems\Meilisearch\IndexQueue\Initializer\Page;
use WapplerSystems\Meilisearch\System\Page\Rootline;
use WapplerSystems\Meilisearch\System\Records\Pages\PagesRepository;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\RootlineUtility;
use TYPO3\CMS\Frontend\Page\PageRepository;
/**
* Extracted logic from the RecordMonitor to trigger mount page updates.
*
* @author Timo Hund <timo.hund@dkd.de>
*/
class MountPagesUpdater
{
/**
* @var PagesRepository
*/
protected $pagesRepository;
/**
* MountPagesUpdater constructor.
*
* @param PagesRepository|null $pagesRepository
*/
public function __construct(PagesRepository $pagesRepository = null) {
$this->pagesRepository = $pagesRepository ?? GeneralUtility::makeInstance(PagesRepository::class);
}
/**
* Handles updates of the Index Queue in case a newly created or changed
* page is part of a tree that is mounted into a another site.
*
* @param int $pageId Page Id (uid).
*/
public function update($pageId)
{
// get the root line of the page, every parent page could be a Mount Page source
$rootlineUtility = GeneralUtility::makeInstance(RootlineUtility::class, $pageId);
try {
$rootLineArray = $rootlineUtility->get();
} catch (\RuntimeException $e) {
$rootLineArray = [];
}
$currentPage = array_shift($rootLineArray);
$currentPageUid = (int)$currentPage['uid'];
if (empty($rootLineArray) && $currentPageUid === 0) {
return;
}
/** @var $rootLine Rootline */
$rootLine = GeneralUtility::makeInstance(Rootline::class, /** @scrutinizer ignore-type */ $rootLineArray);
$rootLineParentPageIds = array_map('intval', $rootLine->getParentPageIds());
$destinationMountProperties = $this->pagesRepository->findMountPointPropertiesByPageIdOrByRootLineParentPageIds($currentPageUid, $rootLineParentPageIds);
if (empty($destinationMountProperties)) {
return;
}
foreach ($destinationMountProperties as $destinationMount) {
$this->addPageToMountingSiteIndexQueue($pageId, $destinationMount);
}
}
/**
* Adds a page to the Index Queue of a site mounting the page.
*
* @param int $mountedPageId ID (uid) of the mounted page.
* @param array $mountProperties Array of mount point properties mountPageSource, mountPageDestination, and mountPageOverlayed
*/
protected function addPageToMountingSiteIndexQueue($mountedPageId, array $mountProperties)
{
$siteRepository = GeneralUtility::makeInstance(SiteRepository::class);
$mountingSite = $siteRepository->getSiteByPageId($mountProperties['mountPageDestination']);
/** @var $pageInitializer Page */
$pageInitializer = GeneralUtility::makeInstance(Page::class);
$pageInitializer->setSite($mountingSite);
$pageInitializer->initializeMountedPage($mountProperties, $mountedPageId);
}
}

View File

@@ -0,0 +1,315 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Index\Queue\RecordMonitor\Helper;
/***************************************************************
* Copyright notice
*
* (c) 2017- Timo Hund <timo.hund@dkd.de>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project is
* free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use WapplerSystems\Meilisearch\Domain\Site\SiteRepository;
use WapplerSystems\Meilisearch\Domain\Site\Site;
use WapplerSystems\Meilisearch\System\Cache\TwoLevelCache;
use WapplerSystems\Meilisearch\System\Configuration\ExtensionConfiguration;
use WapplerSystems\Meilisearch\System\Page\Rootline;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\SingletonInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\RootlineUtility;
use TYPO3\CMS\Frontend\Page\PageRepository;
/**
* RootPageResolver.
*
* Responsibility: The RootPageResolver is responsible to determine all relevant site root page id's
* for a certain records, by table and uid.
*
* @author Timo Hund <timo.hund@dkd.de>
*/
class RootPageResolver implements SingletonInterface
{
/**
* @var ConfigurationAwareRecordService
*/
protected $recordService;
/**
* @var TwoLevelCache
*/
protected $runtimeCache;
/**
* @var ExtensionConfiguration
*/
protected $extensionConfiguration;
/**
* RootPageResolver constructor.
* @param ConfigurationAwareRecordService|null $recordService
* @param TwoLevelCache|null $twoLevelCache
*/
public function __construct(ConfigurationAwareRecordService $recordService = null, TwoLevelCache $twoLevelCache = null)
{
$this->recordService = $recordService ?? GeneralUtility::makeInstance(ConfigurationAwareRecordService::class);
$this->runtimeCache = $twoLevelCache ?? GeneralUtility::makeInstance(TwoLevelCache::class, /** @scrutinizer ignore-type */ 'cache_runtime');
$this->extensionConfiguration = GeneralUtility::makeInstance(ExtensionConfiguration::class);
}
/**
* This method determines the responsible site roots for a record by getting the rootPage of the record and checking
* if the pid is references in another site with additionalPageIds and returning those rootPageIds as well.
* The result is cached by the caching framework.
*
* @param string $table
* @param int $uid
* @return array
*/
public function getResponsibleRootPageIds($table, $uid)
{
$cacheId = 'RootPageResolver' . '_' . 'getResponsibleRootPageIds' . '_' . $table . '_' . $uid;
$methodResult = $this->runtimeCache->get($cacheId);
if (!empty($methodResult)) {
return $methodResult;
}
$methodResult = $this->buildResponsibleRootPageIds($table, $uid);
$this->runtimeCache->set($cacheId, $methodResult);
return $methodResult;
}
/**
* Checks if the passed pageId is a root page.
*
* @param int $pageId Page ID
* @return bool TRUE if the page is marked as root page, FALSE otherwise
*/
public function getIsRootPageId($pageId)
{
// Page 0 can never be a root page
if ($pageId === 0) {
return false;
}
// Page -1 is a workspace thing
if ($pageId === -1) {
return false;
}
$cacheId = 'RootPageResolver' . '_' . 'getIsRootPageId' . '_' . $pageId;
$isSiteRoot = $this->runtimeCache->get($cacheId);
if (!empty($isSiteRoot)) {
return $isSiteRoot;
}
$page = $this->getPageRecordByPageId($pageId);
if (empty($page)) {
throw new \InvalidArgumentException(
'The page for the given page ID \'' . $pageId
. '\' could not be found in the database and can therefore not be used as site root page.',
1487171426
);
}
$isSiteRoot = Site::isRootPage($page);
$this->runtimeCache->set($cacheId, $isSiteRoot);
return $isSiteRoot;
}
/**
* @param $pageId
* @param string $fieldList
* @return array
*/
protected function getPageRecordByPageId($pageId, $fieldList = 'is_siteroot')
{
return (array)BackendUtility::getRecord('pages', $pageId, $fieldList);
}
/**
* Determines the rootpage ID for a given page.
*
* @param int $pageId A page ID somewhere in a tree.
* @param bool $forceFallback Force the explicit detection and do not use the current frontend root line
* @param string $mountPointIdentifier
* @return int The page's tree branch's root page ID
*/
public function getRootPageId($pageId = 0, $forceFallback = false, $mountPointIdentifier = '')
{
/** @var Rootline $rootLine */
$rootLine = GeneralUtility::makeInstance(Rootline::class);
$rootPageId = intval($pageId) ?: intval($GLOBALS['TSFE']->id);
// frontend
if (!empty($GLOBALS['TSFE']->rootLine)) {
$rootLine->setRootLineArray($GLOBALS['TSFE']->rootLine);
}
// fallback, backend
if ($pageId != 0 && ($forceFallback || !$rootLine->getHasRootPage())) {
$rootlineUtility = GeneralUtility::makeInstance(RootlineUtility::class, $pageId, $mountPointIdentifier);
try {
$rootLineArray = $rootlineUtility->get();
} catch (\RuntimeException $e) {
$rootLineArray = [];
}
$rootLine->setRootLineArray($rootLineArray);
}
$rootPageFromRootLine = $rootLine->getRootPageId();
return $rootPageFromRootLine === 0 ? $rootPageId : $rootPageFromRootLine;
}
/**
* This method determines the responsible site roots for a record by getting the rootPage of the record and checking
* if the pid is references in another site with additionalPageIds and returning those rootPageIds as well.
*
* @param string $table
* @param integer $uid
* @return array
*/
protected function buildResponsibleRootPageIds($table, $uid)
{
$rootPages = [];
$rootPageId = $this->getRootPageIdByTableAndUid($table, $uid);
if ($this->getIsRootPageId($rootPageId)) {
$rootPages[] = $rootPageId;
}
if ($this->extensionConfiguration->getIsUseConfigurationTrackRecordsOutsideSiteroot()) {
$recordPageId = $this->getRecordPageId($table, $uid);
if ($recordPageId === 0) {
return $rootPages;
}
$alternativeSiteRoots = $this->getAlternativeSiteRootPagesIds($table, $uid, $recordPageId);
$rootPages = array_merge($rootPages, $alternativeSiteRoots);
}
return $rootPages;
}
/**
* This method checks if the record is a pages record or another one and determines the rootPageId from the records
* rootline.
*
* @param string $table
* @param int $uid
* @return int
*/
protected function getRootPageIdByTableAndUid($table, $uid)
{
if ($table === 'pages') {
$rootPageId = $this->getRootPageId($uid);
return $rootPageId;
} else {
$recordPageId = $this->getRecordPageId($table, $uid);
$rootPageId = $this->getRootPageId($recordPageId, true);
return $rootPageId;
}
}
/**
* Returns the pageId of the record or 0 when no valid record was given.
*
* @param string $table
* @param integer $uid
* @return mixed
*/
protected function getRecordPageId($table, $uid)
{
$record = BackendUtility::getRecord($table, $uid, 'pid');
return !empty($record['pid']) ? (int)$record['pid'] : 0;
}
/**
* When no root page can be determined we check if the pageIdOf the record is configured as additionalPageId in the index
* configuration of another site, if so we return the rootPageId of this site.
* The result is cached by the caching framework.
*
* @param string $table
* @param int $uid
* @param int $recordPageId
* @return array
*/
public function getAlternativeSiteRootPagesIds($table, $uid, $recordPageId)
{
$siteRootsByObservedPageIds = $this->getSiteRootsByObservedPageIds($table, $uid);
if (!isset($siteRootsByObservedPageIds[$recordPageId])) {
return [];
}
return $siteRootsByObservedPageIds[$recordPageId];
}
/**
* Retrieves an optimized array structure we the monitored pageId as key and the relevant site rootIds as value.
*
* @param string $table
* @param integer $uid
* @return array
*/
protected function getSiteRootsByObservedPageIds($table, $uid)
{
$cacheId = 'RootPageResolver' . '_' . 'getSiteRootsByObservedPageIds' . '_' . $table . '_' . $uid;
$methodResult = $this->runtimeCache->get($cacheId);
if (!empty($methodResult)) {
return $methodResult;
}
$methodResult = $this->buildSiteRootsByObservedPageIds($table, $uid);
$this->runtimeCache->set($cacheId, $methodResult);
return $methodResult;
}
/**
* This methods build an array with observer page id as key and rootPageIds as values to determine which root pages
* are responsible for this record by referencing the pageId in additionalPageIds configuration.
*
* @param string $table
* @param integer $uid
* @return array
*/
protected function buildSiteRootsByObservedPageIds($table, $uid)
{
$siteRootByObservedPageIds = [];
$siteRepository = GeneralUtility::makeInstance(SiteRepository::class);
$allSites = $siteRepository->getAvailableSites();
foreach ($allSites as $site) {
$solrConfiguration = $site->getSolrConfiguration();
$indexingConfigurationName = $this->recordService->getIndexingConfigurationName($table, $uid, $solrConfiguration);
if ($indexingConfigurationName === null) {
continue;
}
$observedPageIdsOfSiteRoot = $solrConfiguration->getIndexQueueAdditionalPageIdsByConfigurationName($indexingConfigurationName);
foreach ($observedPageIdsOfSiteRoot as $observedPageIdOfSiteRoot) {
$siteRootByObservedPageIds[$observedPageIdOfSiteRoot][] = $site->getRootPageId();
}
}
return $siteRootByObservedPageIds;
}
}

View File

@@ -0,0 +1,138 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Index\Queue\Statistic;
/***************************************************************
* Copyright notice
*
* (c) 2017 Timo Hund <timo.hund@dkd.de>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project is
* free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
class QueueStatistic
{
/**
* @var int
*/
protected $failedCount = 0;
/**
* @var int
*/
protected $pendingCount = 0;
/**
* @var int
*/
protected $successCount = 0;
/**
* @param int $failedCount
*/
public function setFailedCount($failedCount)
{
$this->failedCount = $failedCount;
}
/**
* @return int
*/
public function getFailedCount()
{
return $this->failedCount;
}
/**
* @return float|int
*/
public function getFailedPercentage()
{
return $this->getPercentage($this->getFailedCount());
}
/**
* @param int $pendingCount
*/
public function setPendingCount($pendingCount)
{
$this->pendingCount = $pendingCount;
}
/**
* @return int
*/
public function getPendingCount()
{
return $this->pendingCount;
}
/**
* @return float|int
*/
public function getPendingPercentage()
{
return $this->getPercentage($this->getPendingCount());
}
/**
* @param int $successCount
*/
public function setSuccessCount($successCount)
{
$this->successCount = $successCount;
}
/**
* @return int
*/
public function getSuccessCount()
{
return $this->successCount;
}
/**
* @return float|int
*/
public function getSuccessPercentage()
{
return $this->getPercentage($this->getSuccessCount());
}
/**
* @return int
*/
public function getTotalCount()
{
return $this->pendingCount + $this->failedCount + $this->successCount;
}
/**
* @param integer $count
* @return float
*/
protected function getPercentage($count)
{
$total = $this->getTotalCount();
if ($total === 0) {
return 0.0;
}
return (float)round((100 / $total) * $count, 2);
}
}

View File

@@ -0,0 +1,108 @@
<?php declare(strict_types = 1);
namespace WapplerSystems\Meilisearch\Domain\Index\Queue\Statistic;
/***************************************************************
* Copyright notice
*
* (c) 2010-2017 dkd Internet Service GmbH <solr-support@dkd.de>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project is
* free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use WapplerSystems\Meilisearch\System\Records\AbstractRepository;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Persistence\Generic\Exception\UnsupportedMethodException;
/**
* Class QueueStatisticsRepository
*/
class QueueStatisticsRepository extends AbstractRepository
{
/**
* @var string
*/
protected $table = 'tx_meilisearch_indexqueue_item';
/**
* Extracts the number of pending, indexed and erroneous items from the
* Index Queue.
*
* @param int $rootPid
* @param string $indexingConfigurationName
*
* @return QueueStatistic
*/
public function findOneByRootPidAndOptionalIndexingConfigurationName(int $rootPid, $indexingConfigurationName = null): QueueStatistic
{
$queryBuilder = $this->getQueryBuilder();
$queryBuilder
->add('select', vsprintf('(%s < %s) AS %s', [
$queryBuilder->quoteIdentifier('indexed'),
$queryBuilder->quoteIdentifier('changed'),
$queryBuilder->quoteIdentifier('pending')
]), true)
->add('select', vsprintf('(%s) AS %s', [
$queryBuilder->expr()->notLike('errors', $queryBuilder->createNamedParameter('')),
$queryBuilder->quoteIdentifier('failed')
]), true)
->add('select', $queryBuilder->expr()->count('*', 'count'), true)
->from($this->table)
->where($queryBuilder->expr()->eq('root', $queryBuilder->createNamedParameter($rootPid, \PDO::PARAM_INT)))
->groupBy('pending', 'failed');
if (!empty($indexingConfigurationName)) {
$queryBuilder->andWhere($queryBuilder->expr()->eq('indexing_configuration', $queryBuilder->createNamedParameter($indexingConfigurationName)));
}
return $this->buildQueueStatisticFromResultSet($queryBuilder->execute()->fetchAll());
}
/**
* Instantiates and fills QueueStatistic with values
*
* @param array $indexQueueStatisticResultSet
* @return QueueStatistic
*/
protected function buildQueueStatisticFromResultSet(array $indexQueueStatisticResultSet): QueueStatistic
{
/* @var $statistic QueueStatistic */
$statistic = GeneralUtility::makeInstance(QueueStatistic::class);
foreach ($indexQueueStatisticResultSet as $row) {
if ($row['failed'] == 1) {
$statistic->setFailedCount((int)$row['count']);
} elseif ($row['pending'] == 1) {
$statistic->setPendingCount((int)$row['count']);
} else {
$statistic->setSuccessCount((int)$row['count']);
}
}
return $statistic;
}
/**
* Don't use this method.
*
* @return int
* @throws UnsupportedMethodException
*/
public function count(): int
{
throw new UnsupportedMethodException('Can not count the Index Queue Statistics.', 1504694750);
}
}

View File

@@ -0,0 +1,312 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ApacheSolrDocument;
/***************************************************************
* Copyright notice
*
* (c) 2017 Timo Hund <timo.hund@dkd.de>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project is
* free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use WapplerSystems\Meilisearch\Access\Rootline;
use WapplerSystems\Meilisearch\Domain\Site\SiteRepository;
use WapplerSystems\Meilisearch\Domain\Variants\IdBuilder;
use WapplerSystems\Meilisearch\Domain\Site\Site;
use WapplerSystems\Meilisearch\System\Solr\Document\Document;
use WapplerSystems\Meilisearch\Typo3PageContentExtractor;
use WapplerSystems\Meilisearch\Util;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
/**
* Builder class to build an ApacheSolrDocument
*
* Responsible to build \WapplerSystems\Meilisearch\System\Solr\Document\Document
*
* @author Timo Hund <timo.hund@dkd.de>
*/
class Builder
{
/**
* @var IdBuilder
*/
protected $variantIdBuilder;
/**
* Builder constructor.
* @param IdBuilder|null $variantIdBuilder
*/
public function __construct(IdBuilder $variantIdBuilder = null)
{
$this->variantIdBuilder = $variantIdBuilder ?? GeneralUtility::makeInstance(IdBuilder::class);
}
/**
* This method can be used to build an Document from a TYPO3 page.
*
* @param TypoScriptFrontendController $page
* @param string $url
* @param Rootline $pageAccessRootline
* @param string $mountPointParameter
* @return Document|object
*/
public function fromPage(TypoScriptFrontendController $page, $url, Rootline $pageAccessRootline, $mountPointParameter): Document
{
/* @var $document Document */
$document = GeneralUtility::makeInstance(Document::class);
$site = $this->getSiteByPageId($page->id);
$pageRecord = $page->page;
$accessGroups = $this->getDocumentIdGroups($pageAccessRootline);
$documentId = $this->getPageDocumentId($page, $accessGroups, $mountPointParameter);
$document->setField('id', $documentId);
$document->setField('site', $site->getDomain());
$document->setField('siteHash', $site->getSiteHash());
$document->setField('appKey', 'EXT:meilisearch');
$document->setField('type', 'pages');
// system fields
$document->setField('uid', $page->id);
$document->setField('pid', $pageRecord['pid']);
// variantId
$variantId = $this->variantIdBuilder->buildFromTypeAndUid('pages', $page->id);
$document->setField('variantId', $variantId);
$document->setField('typeNum', $page->type);
$document->setField('created', $pageRecord['crdate']);
$document->setField('changed', $pageRecord['SYS_LASTCHANGED']);
$rootline = $this->getRootLineFieldValue($page->id, $mountPointParameter);
$document->setField('rootline', $rootline);
// access
$this->addAccessField($document, $pageAccessRootline);
$this->addEndtimeField($document, $pageRecord);
// content
// @extensionScannerIgnoreLine
$contentExtractor = $this->getExtractorForPageContent($page->content);
$document->setField('title', $contentExtractor->getPageTitle());
$document->setField('subTitle', $pageRecord['subtitle']);
$document->setField('navTitle', $pageRecord['nav_title']);
$document->setField('author', $pageRecord['author']);
$document->setField('description', $pageRecord['description']);
$document->setField('abstract', $pageRecord['abstract']);
$document->setField('content', $contentExtractor->getIndexableContent());
$document->setField('url', $url);
$this->addKeywordsField($document, $pageRecord);
$this->addTagContentFields($document, $contentExtractor->getTagContent());
return $document;
}
/**
* Creates a Solr document with the basic / core fields set already.
*
* @param array $itemRecord
* @param string $type
* @param int $rootPageUid
* @param string $accessRootLine
* @return Document
*/
public function fromRecord(array $itemRecord, string $type, int $rootPageUid, string $accessRootLine): Document
{
/* @var $document Document */
$document = GeneralUtility::makeInstance(Document::class);
$site = $this->getSiteByPageId($rootPageUid);
$documentId = $this->getDocumentId($type, $site->getRootPageId(), $itemRecord['uid']);
// required fields
$document->setField('id', $documentId);
$document->setField('type', $type);
$document->setField('appKey', 'EXT:meilisearch');
// site, siteHash
$document->setField('site', $site->getDomain());
$document->setField('siteHash', $site->getSiteHash());
// uid, pid
$document->setField('uid', $itemRecord['uid']);
$document->setField('pid', $itemRecord['pid']);
// variantId
$variantId = $this->variantIdBuilder->buildFromTypeAndUid($type, $itemRecord['uid']);
$document->setField('variantId', $variantId);
// created, changed
if (!empty($GLOBALS['TCA'][$type]['ctrl']['crdate'])) {
$document->setField('created', $itemRecord[$GLOBALS['TCA'][$type]['ctrl']['crdate']]);
}
if (!empty($GLOBALS['TCA'][$type]['ctrl']['tstamp'])) {
$document->setField('changed', $itemRecord[$GLOBALS['TCA'][$type]['ctrl']['tstamp']]);
}
// access, endtime
$document->setField('access', $accessRootLine);
if (!empty($GLOBALS['TCA'][$type]['ctrl']['enablecolumns']['endtime'])
&& $itemRecord[$GLOBALS['TCA'][$type]['ctrl']['enablecolumns']['endtime']] != 0
) {
$document->setField('endtime', $itemRecord[$GLOBALS['TCA'][$type]['ctrl']['enablecolumns']['endtime']]);
}
return $document;
}
/**
* @param TypoScriptFrontendController $page
* @param string $accessGroups
* @param string $mountPointParameter
* @return string
*/
protected function getPageDocumentId(TypoScriptFrontendController $frontendController, string $accessGroups, string $mountPointParameter): string
{
return Util::getPageDocumentId($frontendController->id, $frontendController->type, Util::getLanguageUid(), $accessGroups, $mountPointParameter);
}
/**
* @param string $type
* @param int $rootPageId
* @param int $recordUid
* @return string
*/
protected function getDocumentId(string $type, int $rootPageId, int $recordUid): string
{
return Util::getDocumentId($type, $rootPageId, $recordUid);
}
/**
* @param integer $pageId
* @return Site
*/
protected function getSiteByPageId($pageId)
{
$siteRepository = GeneralUtility::makeInstance(SiteRepository::class);
return $siteRepository->getSiteByPageId($pageId);
}
/**
* @param string $pageContent
* @return Typo3PageContentExtractor
*/
protected function getExtractorForPageContent($pageContent)
{
return GeneralUtility::makeInstance(Typo3PageContentExtractor::class, /** @scrutinizer ignore-type */ $pageContent);
}
/**
* Builds the content for the rootline field.
*
* @param int $pageId
* @param string $mountPointParameter
* @return string
*/
protected function getRootLineFieldValue($pageId, $mountPointParameter)
{
$rootline = $pageId;
if ($mountPointParameter !== '') {
$rootline .= ',' . $mountPointParameter;
}
return $rootline;
}
/**
* Gets a comma separated list of frontend user groups to use for the
* document ID.
*
* @param Rootline $pageAccessRootline
* @return string A comma separated list of frontend user groups.
*/
protected function getDocumentIdGroups(Rootline $pageAccessRootline)
{
$groups = $pageAccessRootline->getGroups();
$groups = Rootline::cleanGroupArray($groups);
if (empty($groups)) {
$groups[] = 0;
}
$groups = implode(',', $groups);
return $groups;
}
/**
* Adds the access field to the document if needed.
*
* @param Document $document
* @param Rootline $pageAccessRootline
*/
protected function addAccessField(Document $document, Rootline $pageAccessRootline)
{
$access = (string)$pageAccessRootline;
if (trim($access) !== '') {
$document->setField('access', $access);
}
}
/**
* Adds the endtime field value to the Document.
*
* @param Document $document
* @param array $pageRecord
*/
protected function addEndtimeField(Document $document, $pageRecord)
{
if ($pageRecord['endtime']) {
$document->setField('endtime', $pageRecord['endtime']);
}
}
/**
* Adds keywords, multi valued.
*
* @param Document $document
* @param array $pageRecord
*/
protected function addKeywordsField(Document $document, $pageRecord)
{
if (!isset($pageRecord['keywords'])) {
return;
}
$keywords = array_unique(GeneralUtility::trimExplode(',', $pageRecord['keywords'], true));
foreach ($keywords as $keyword) {
$document->addField('keywords', $keyword);
}
}
/**
* Add content from several tags like headers, anchors, ...
*
* @param Document $document
* @param array $tagContent
*/
protected function addTagContentFields(Document $document, $tagContent = [])
{
foreach ($tagContent as $fieldName => $fieldValue) {
$document->setField($fieldName, $fieldValue);
}
}
}

View File

@@ -0,0 +1,170 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ApacheSolrDocument;
/***************************************************************
* Copyright notice
*
* (c) 2017 Rafael Kähm <rafael.kaehm@dkd.de>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project is
* free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use WapplerSystems\Meilisearch\ConnectionManager;
use WapplerSystems\Meilisearch\Domain\Search\Query\QueryBuilder;
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Result\Parser\DocumentEscapeService;
use WapplerSystems\Meilisearch\Domain\Site\SiteRepository;
use WapplerSystems\Meilisearch\NoSolrConnectionFoundException;
use WapplerSystems\Meilisearch\Search;
use WapplerSystems\Meilisearch\System\Configuration\TypoScriptConfiguration;
use WapplerSystems\Meilisearch\System\Solr\Document\Document;
use WapplerSystems\Meilisearch\System\Solr\SolrCommunicationException;
use WapplerSystems\Meilisearch\System\Solr\SolrConnection;
use WapplerSystems\Meilisearch\Util;
use TYPO3\CMS\Core\SingletonInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Class Repository
*/
class Repository implements SingletonInterface
{
/**
* Search
*
* @var \WapplerSystems\Meilisearch\Search
*/
protected $search;
/**
* @var DocumentEscapeService
*/
protected $documentEscapeService = null;
/**
* @var TypoScriptConfiguration|null
*/
protected $typoScriptConfiguration = null;
/**
* @var QueryBuilder
*/
protected $queryBuilder;
/**
* Repository constructor.
* @param DocumentEscapeService|null $documentEscapeService
* @param QueryBuilder|null $queryBuilder
*/
public function __construct(DocumentEscapeService $documentEscapeService = null, TypoScriptConfiguration $typoScriptConfiguration = null, QueryBuilder $queryBuilder = null)
{
$this->typoScriptConfiguration = $typoScriptConfiguration ?? Util::getSolrConfiguration();
$this->documentEscapeService = $documentEscapeService ?? GeneralUtility::makeInstance(DocumentEscapeService::class, /** @scrutinizer ignore-type */ $typoScriptConfiguration);
$this->queryBuilder = $queryBuilder ?? GeneralUtility::makeInstance(QueryBuilder::class, /** @scrutinizer ignore-type */ $this->typoScriptConfiguration);
}
/**
* Returns firs found \WapplerSystems\Meilisearch\System\Solr\Document\Document for current page by given language id.
*
* @param $languageId
* @return Document|false
*/
public function findOneByPageIdAndByLanguageId($pageId, $languageId)
{
$documentCollection = $this->findByPageIdAndByLanguageId($pageId, $languageId);
return reset($documentCollection);
}
/**
* Returns all found \WapplerSystems\Meilisearch\System\Solr\Document\Document[] by given page id and language id.
* Returns empty array if nothing found, e.g. if no language or no page(or no index for page) is present.
*
* @param int $pageId
* @param int $languageId
* @return Document[]
*/
public function findByPageIdAndByLanguageId($pageId, $languageId)
{
try {
$this->initializeSearch($pageId, $languageId);
$pageQuery = $this->queryBuilder->buildPageQuery($pageId);
$response = $this->search->search($pageQuery, 0, 10000);
} catch (NoSolrConnectionFoundException $exception) {
return [];
} catch (SolrCommunicationException $exception) {
return [];
}
$data = $response->getParsedData();
// @extensionScannerIgnoreLine
return $this->documentEscapeService->applyHtmlSpecialCharsOnAllFields($data->response->docs ?? []);
}
/**
* @param string $type
* @param int $uid
* @param int $pageId
* @param int $languageId
* @return Document[]|array
*/
public function findByTypeAndPidAndUidAndLanguageId($type, $uid, $pageId, $languageId): array
{
try {
$this->initializeSearch($pageId, $languageId);
$recordQuery = $this->queryBuilder->buildRecordQuery($type, $uid, $pageId);
$response = $this->search->search($recordQuery, 0, 10000);
} catch (NoSolrConnectionFoundException $exception) {
return [];
} catch (SolrCommunicationException $exception) {
return [];
}
$data = $response->getParsedData();
// @extensionScannerIgnoreLine
return $this->documentEscapeService->applyHtmlSpecialCharsOnAllFields($data->response->docs ?? []);
}
/**
* Initializes Search for given language
*
* @param int $languageId
*/
protected function initializeSearch($pageId, $languageId = 0)
{
if (!is_int($pageId)) {
throw new \InvalidArgumentException('Invalid page ID = ' . $pageId, 1487332926);
}
if (!is_int($languageId)) { // @todo: Check if lang id is defined and present?
throw new \InvalidArgumentException('Invalid language ID = ' . $languageId, 1487335178);
}
/* @var $connectionManager ConnectionManager */
$connectionManager = GeneralUtility::makeInstance(ConnectionManager::class);
$solrConnection = $connectionManager->getConnectionByPageId($pageId, $languageId);
$this->search = $this->getSearch($solrConnection);
}
/**
* Retrieves an instance of the Search object.
*
* @param SolrConnection $solrConnection
* @return Search
*/
protected function getSearch($solrConnection)
{
return GeneralUtility::makeInstance(Search::class, /** @scrutinizer ignore-type */ $solrConnection);
}
}

View File

@@ -0,0 +1,179 @@
<?php declare(strict_types = 1);
namespace WapplerSystems\Meilisearch\Domain\Search\FrequentSearches;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use WapplerSystems\Meilisearch\Domain\Search\Statistics\StatisticsRepository;
use WapplerSystems\Meilisearch\System\Configuration\TypoScriptConfiguration;
use WapplerSystems\Meilisearch\Util;
use TYPO3\CMS\Core\Cache\Frontend\AbstractFrontend;
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
/**
* The FrequentSearchesService is used to retrieve the frequent searches from the database or cache.
*
* @author Dimitri Ebert <dimitri.ebert@dkd.de>
* @author Timo Schmidt <timo.schmidt@dkd.de>
* @copyright (c) 2015-2021 dkd Internet Service GmbH <info@dkd.de>
*/
class FrequentSearchesService
{
/**
* Instance of the caching frontend used to cache this command's output.
*
* @var AbstractFrontend
*/
protected $cache;
/**
* @var TypoScriptFrontendController
*/
protected $tsfe;
/**
* @var StatisticsRepository
*/
protected $statisticsRepository;
/**
* @var TypoScriptConfiguration
*/
protected $configuration;
/**
* @param TypoScriptConfiguration $typoscriptConfiguration
* @param AbstractFrontend|null $cache
* @param TypoScriptFrontendController|null $tsfe
* @param StatisticsRepository|null $statisticsRepository
*/
public function __construct(
TypoScriptConfiguration $typoscriptConfiguration,
AbstractFrontend $cache = null,
TypoScriptFrontendController $tsfe = null,
StatisticsRepository $statisticsRepository = null
) {
$this->configuration = $typoscriptConfiguration;
$this->cache = $cache;
$this->tsfe = $tsfe;
$this->statisticsRepository = $statisticsRepository ?? GeneralUtility::makeInstance(StatisticsRepository::class);
}
/**
* Generates an array with terms and hits
*
* @return array Tags as array with terms and hits
*/
public function getFrequentSearchTerms() : array
{
$frequentSearchConfiguration = $this->configuration->getSearchFrequentSearchesConfiguration();
$identifier = $this->getCacheIdentifier($frequentSearchConfiguration);
if ($this->hasValidCache() && $this->cache->has($identifier)) {
$terms = $this->cache->get($identifier);
} else {
$terms = $this->getFrequentSearchTermsFromStatistics($frequentSearchConfiguration);
if ($frequentSearchConfiguration['sortBy'] === 'hits') {
arsort($terms);
} else {
ksort($terms);
}
$lifetime = null;
if (isset($frequentSearchConfiguration['cacheLifetime'])) {
$lifetime = intval($frequentSearchConfiguration['cacheLifetime']);
}
if ($this->hasValidCache()) {
$this->cache->set($identifier, $terms, [], $lifetime);
}
}
return $terms;
}
/**
* Gets frequent search terms from the statistics tracking table.
*
* @param array $frequentSearchConfiguration
* @return array Array of frequent search terms, keys are the terms, values are hits
*/
protected function getFrequentSearchTermsFromStatistics(array $frequentSearchConfiguration) : array
{
$terms = [];
if ($frequentSearchConfiguration['select.']['checkRootPageId']) {
$checkRootPidWhere = 'root_pid = ' . $this->tsfe->tmpl->rootLine[0]['uid'];
} else {
$checkRootPidWhere = '1';
}
if ($frequentSearchConfiguration['select.']['checkLanguage']) {
$checkLanguageWhere = ' AND language =' . Util::getLanguageUid();
} else {
$checkLanguageWhere = '';
}
$frequentSearchConfiguration['select.']['ADD_WHERE'] = $checkRootPidWhere .
$checkLanguageWhere . ' ' .
$frequentSearchConfiguration['select.']['ADD_WHERE'];
$frequentSearchTerms = $this->statisticsRepository
->getFrequentSearchTermsFromStatisticsByFrequentSearchConfiguration($frequentSearchConfiguration);
if (!is_array($frequentSearchTerms)) {
return $terms;
}
foreach ($frequentSearchTerms as $term) {
$cleanedTerm = html_entity_decode($term['search_term'], ENT_QUOTES, 'UTF-8');
$terms[$cleanedTerm] = $term['hits'];
}
return $terms;
}
/**
* @param array $frequentSearchConfiguration
* @return string
*/
protected function getCacheIdentifier(array $frequentSearchConfiguration) : string
{
// Use configuration as cache identifier
$identifier = 'frequentSearchesTags';
if ($frequentSearchConfiguration['select.']['checkRootPageId']) {
$identifier .= '_RP' . (int)$this->tsfe->tmpl->rootLine[0]['uid'];
}
if ($frequentSearchConfiguration['select.']['checkLanguage']) {
$identifier .= '_L' . Util::getLanguageUid();
}
$identifier .= '_' . md5(serialize($frequentSearchConfiguration));
return $identifier;
}
/**
* Checks if this service has a valid cache class
*
* @return bool
*/
protected function hasValidCache(): bool
{
return ($this->cache instanceof FrontendInterface);
}
}

View File

@@ -0,0 +1,60 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\Highlight;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
/**
* Provides highlighting of the search words on the document's actual page by
* adding parameters to a document's URL property.
*
* Initial code from WapplerSystems\Meilisearch\ResultDocumentModifier\SiteHighlighter
*
* @author Stefan Sprenger <stefan.sprenger@dkd.de>
* @author Timo Hund <timo.hund@dkd.de>
*/
use WapplerSystems\Meilisearch\System\Url\UrlHelper;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Class SiteHighlighterUrlModifier
*/
class SiteHighlighterUrlModifier {
/**
* @param string $url
* @param string $searchWords
* @param boolean $addNoCache
* @param boolean $keepCHash
* @return string
*/
public function modify($url, $searchWords, $addNoCache = true, $keepCHash = false) {
$searchWords = str_replace('&quot;', '', $searchWords);
$searchWords = GeneralUtility::trimExplode(' ', $searchWords, true);
/** @var UrlHelper $urlHelper */
$urlHelper = GeneralUtility::makeInstance(UrlHelper::class, /** @scrutinizer ignore-type */ $url);
$urlHelper->addQueryParameter('sword_list', $searchWords);
if ($addNoCache) {
$urlHelper->addQueryParameter('no_cache', '1');
}
if (!$keepCHash) {
$urlHelper->removeQueryParameter('cHash');
}
return $urlHelper->getUrl();
}
}

View File

@@ -0,0 +1,156 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\LastSearches;
/***************************************************************
* Copyright notice
*
* (c) 2010-2017 dkd Internet Service GmbH <solr-support@dkd.de>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project is
* free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use WapplerSystems\Meilisearch\System\Records\AbstractRepository;
class LastSearchesRepository extends AbstractRepository
{
/**
* @var string
*/
protected $table = 'tx_meilisearch_last_searches';
/**
* Finds the last searched keywords from the database
*
* @param int $limit
* @return array An array containing the last searches of the current user
*/
public function findAllKeywords($limit = 10) : array
{
$lastSearchesResultSet = $this->getLastSearchesResultSet($limit);
if (empty($lastSearchesResultSet)) {
return [];
}
$lastSearches = [];
foreach ($lastSearchesResultSet as $row) {
$lastSearches[] = html_entity_decode($row['keywords'], ENT_QUOTES, 'UTF-8');
}
return $lastSearches;
}
/**
* Returns all last searches
*
* @param $limit
* @return array
*/
protected function getLastSearchesResultSet($limit) : array
{
$queryBuilder = $this->getQueryBuilder();
return $queryBuilder
->select('keywords')
->addSelectLiteral(
$queryBuilder->expr()->max('tstamp','maxtstamp')
)
->from($this->table)
// There is no support for DISTINCT, a ->groupBy() has to be used instead.
->groupBy('keywords')
->orderBy('maxtstamp', 'DESC')
->setMaxResults($limit)->execute()->fetchAll();
}
/**
* Adds keywords to last searches or updates the oldest row by given limit.
*
* @param string $lastSearchesKeywords
* @param int $lastSearchesLimit
* @return void
*/
public function add(string $lastSearchesKeywords, int $lastSearchesLimit)
{
$nextSequenceId = $this->resolveNextSequenceIdForGivenLimit($lastSearchesLimit);
$rowsCount = $this->count();
if ($nextSequenceId < $rowsCount) {
$this->update([
'sequence_id' => $nextSequenceId,
'keywords' => $lastSearchesKeywords
]);
return;
}
$queryBuilder = $this->getQueryBuilder();
$queryBuilder
->insert($this->table)
->values([
'sequence_id' => $nextSequenceId,
'keywords' => $lastSearchesKeywords,
'tstamp' => time()
])
->execute();
}
/**
* Resolves next sequence id by given last searches limit.
*
* @param int $lastSearchesLimit
* @return int
*/
protected function resolveNextSequenceIdForGivenLimit(int $lastSearchesLimit) : int
{
$nextSequenceId = 0;
$queryBuilder = $this->getQueryBuilder();
$result = $queryBuilder->select('sequence_id')
->from($this->table)
->orderBy('tstamp', 'DESC')
->setMaxResults(1)
->execute()->fetch();
if (!empty($result)) {
$nextSequenceId = ($result['sequence_id'] + 1) % $lastSearchesLimit;
}
return $nextSequenceId;
}
/**
* Updates last searches row by using sequence_id from given $lastSearchesRow array
*
* @param array $lastSearchesRow
* @return void
* @throws \InvalidArgumentException
*/
protected function update(array $lastSearchesRow)
{
$queryBuilder = $this->getQueryBuilder();
$affectedRows = $queryBuilder
->update($this->table)
->where(
$queryBuilder->expr()->eq('sequence_id', $queryBuilder->createNamedParameter($lastSearchesRow['sequence_id']))
)
->set('tstamp', time())
->set('keywords', $lastSearchesRow['keywords'])
->execute();
if ($affectedRows < 1) {
throw new \InvalidArgumentException(vsprintf('By trying to update last searches row with values "%s" nothing was updated, make sure the given "sequence_id" exists in database.', [\json_encode($lastSearchesRow)]), 1502717923);
}
}
}

View File

@@ -0,0 +1,148 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\LastSearches;
/***************************************************************
* Copyright notice
*
* (c) 2015-2016 Timo Schmidt <timo.schmidt@dkd.de>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project is
* free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use WapplerSystems\Meilisearch\System\Configuration\TypoScriptConfiguration;
use WapplerSystems\Meilisearch\System\Session\FrontendUserSession;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* The LastSearchesService is responsible to return the LastSearches from the session or database,
* depending on the configuration.
*
* @author Timo Schmidt <timo.schmidt@dkd.de>
*/
class LastSearchesService
{
/**
* @var TypoScriptConfiguration
*/
protected $configuration;
/**
* @var FrontendUserSession
*/
protected $session;
/**
* @var LastSearchesRepository
*/
protected $lastSearchesRepository;
/**
* @param TypoScriptConfiguration $typoscriptConfiguration
* @param FrontendUserSession|null $session
* @param LastSearchesRepository|null $lastSearchesRepository
*/
public function __construct(TypoScriptConfiguration $typoscriptConfiguration, FrontendUserSession $session = null, LastSearchesRepository $lastSearchesRepository = null)
{
$this->configuration = $typoscriptConfiguration;
$this->session = $session ?? GeneralUtility::makeInstance(FrontendUserSession::class);
$this->lastSearchesRepository = $lastSearchesRepository ?? GeneralUtility::makeInstance(LastSearchesRepository::class);
}
/**
* Retrieves the last searches from the session or database depending on the configuration.
*
* @return array
*/
public function getLastSearches()
{
$lastSearchesKeywords = [];
$mode = $this->configuration->getSearchLastSearchesMode();
$limit = $this->configuration->getSearchLastSearchesLimit();
switch ($mode) {
case 'user':
$lastSearchesKeywords = $this->getLastSearchesFromSession($limit);
break;
case 'global':
$lastSearchesKeywords = $this->lastSearchesRepository->findAllKeywords($limit);
break;
}
return $lastSearchesKeywords;
}
/**
* Saves the keywords to the last searches in the database or session depending on the configuration.
*
* @param string $keywords
* @throws \UnexpectedValueException
*/
public function addToLastSearches($keywords)
{
$mode = $this->configuration->getSearchLastSearchesMode();
switch ($mode) {
case 'user':
$this->storeKeywordsToSession($keywords);
break;
case 'global':
$this->lastSearchesRepository->add($keywords, (int)$this->configuration->getSearchLastSearchesLimit());
break;
default:
throw new \UnexpectedValueException(
'Unknown mode for plugin.tx_meilisearch.search.lastSearches.mode, valid modes are "user" or "global".',
1342456570
);
}
}
/**
* Gets the last searched keywords from the user's session
*
* @param int $limit
* @return array An array containing the last searches of the current user
*/
protected function getLastSearchesFromSession($limit)
{
$lastSearches = $this->session->getLastSearches();
$lastSearches = array_slice(array_reverse(array_unique($lastSearches)), 0, $limit);
return $lastSearches;
}
/**
* Stores the keywords from the current query to the user's session.
*
* @param string $keywords The current query's keywords
* @return void
*/
protected function storeKeywordsToSession($keywords)
{
$currentLastSearches = $this->session->getLastSearches();
$lastSearches = $currentLastSearches;
$newLastSearchesCount = array_push($lastSearches, $keywords);
while ($newLastSearchesCount > $this->configuration->getSearchLastSearchesLimit()) {
array_shift($lastSearches);
$newLastSearchesCount = count($lastSearches);
}
$this->session->setLastSearches($lastSearches);
}
}

View File

@@ -0,0 +1,74 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\LastSearches;
/***************************************************************
* Copyright notice
*
* (c) 2017 Timo Hund <timo.hund@dkd.de>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project is
* free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use WapplerSystems\Meilisearch\Domain\Search\LastSearches\LastSearchesService;
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\SearchResultSet;
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\SearchResultSetProcessor;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Writes the last searches
*
* @author Timo Hund <timo.hund@dkd.de>
*/
class LastSearchesWriterProcessor implements SearchResultSetProcessor
{
/**
* @param SearchResultSet $resultSet
* @return SearchResultSet
*/
public function process(SearchResultSet $resultSet) {
if ($resultSet->getAllResultCount() === 0) {
// when the search does not produce a result we do not store the last searches
return $resultSet;
}
if (!isset($GLOBALS['TSFE'])) {
return $resultSet;
}
$query = $resultSet->getUsedSearchRequest()->getRawUserQuery();
if (is_string($query)) {
$lastSearchesService = $this->getLastSearchesService($resultSet);
$lastSearchesService->addToLastSearches($query);
}
return $resultSet;
}
/**
* @param SearchResultSet $resultSet
* @return LastSearchesService
*/
protected function getLastSearchesService(SearchResultSet $resultSet) {
return GeneralUtility::makeInstance(LastSearchesService::class,
/** @scrutinizer ignore-type */ $resultSet->getUsedSearchRequest()->getContextTypoScriptConfiguration());
}
}

View File

@@ -0,0 +1,558 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\Query;
/***************************************************************
* Copyright notice
*
* (c) 2017 <timo.hund@dkd.de>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project is
* free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use WapplerSystems\Meilisearch\Domain\Search\Query\ParameterBuilder\BigramPhraseFields;
use WapplerSystems\Meilisearch\Domain\Search\Query\ParameterBuilder\Elevation;
use WapplerSystems\Meilisearch\Domain\Search\Query\ParameterBuilder\Faceting;
use WapplerSystems\Meilisearch\Domain\Search\Query\ParameterBuilder\FieldCollapsing;
use WapplerSystems\Meilisearch\Domain\Search\Query\ParameterBuilder\Grouping;
use WapplerSystems\Meilisearch\Domain\Search\Query\ParameterBuilder\Highlighting;
use WapplerSystems\Meilisearch\Domain\Search\Query\ParameterBuilder\Operator;
use WapplerSystems\Meilisearch\Domain\Search\Query\ParameterBuilder\PhraseFields;
use WapplerSystems\Meilisearch\Domain\Search\Query\ParameterBuilder\QueryFields;
use WapplerSystems\Meilisearch\Domain\Search\Query\ParameterBuilder\ReturnFields;
use WapplerSystems\Meilisearch\Domain\Search\Query\ParameterBuilder\Slops;
use WapplerSystems\Meilisearch\Domain\Search\Query\ParameterBuilder\Sorting;
use WapplerSystems\Meilisearch\Domain\Search\Query\ParameterBuilder\Sortings;
use WapplerSystems\Meilisearch\Domain\Search\Query\ParameterBuilder\Spellchecking;
use WapplerSystems\Meilisearch\Domain\Search\Query\ParameterBuilder\TrigramPhraseFields;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
/**
* The AbstractQueryBuilder contains all logic to initialize solr queries independent from TYPO3.
*/
abstract class AbstractQueryBuilder {
/**
* @var Query
*/
protected $queryToBuild = null;
/**
* @param Query $query
* @return $this
*/
public function startFrom(Query $query)
{
$this->queryToBuild = $query;
return $this;
}
/**
* @return Query
*/
public function getQuery(): Query
{
return $this->queryToBuild;
}
/**
* @param bool $omitHeader
* @return $this
*/
public function useOmitHeader($omitHeader = true)
{
$this->queryToBuild->setOmitHeader($omitHeader);
return $this;
}
/**
* Uses an array of filters and applies them to the query.
*
* @param array $filterArray
* @return $this
*/
public function useFilterArray(array $filterArray)
{
foreach ($filterArray as $key => $additionalFilter) {
$this->useFilter($additionalFilter, $key);
}
return $this;
}
/**
* Applies the queryString that is used to search
*
* @param string $queryString
* @return $this
*/
public function useQueryString($queryString)
{
$this->queryToBuild->setQuery($queryString);
return $this;
}
/**
* Applies the passed queryType to the query.
*
* @param string $queryType
* @return $this
*/
public function useQueryType(string $queryType)
{
$this->queryToBuild->addParam('qt', $queryType);
return $this;
}
/**
* Remove the queryType (qt) from the query.
*
* @return $this
*/
public function removeQueryType()
{
$this->queryToBuild->addParam('qt', null);
return $this;
}
/**
* Can be used to remove all sortings from the query.
*
* @return $this
*/
public function removeAllSortings()
{
$this->queryToBuild->clearSorts();
return $this;
}
/**
* Applies the passed sorting to the query.
*
* @param Sorting $sorting
* @return $this
*/
public function useSorting(Sorting $sorting)
{
if (strpos($sorting->getFieldName(), 'relevance') !== false) {
$this->removeAllSortings();
return $this;
}
$this->queryToBuild->addSort($sorting->getFieldName(), $sorting->getDirection());
return $this;
}
/**
* Applies the passed sorting to the query.
*
* @param Sortings $sortings
* @return $this
*/
public function useSortings(Sortings $sortings)
{
foreach($sortings->getSortings() as $sorting) {
$this->useSorting($sorting);
}
return $this;
}
/**
* @param int $resultsPerPage
* @return $this
*/
public function useResultsPerPage($resultsPerPage)
{
$this->queryToBuild->setRows($resultsPerPage);
return $this;
}
/**
* @param int $page
* @return $this
*/
public function usePage($page)
{
$this->queryToBuild->setStart($page);
return $this;
}
/**
* @param Operator $operator
* @return $this
*/
public function useOperator(Operator $operator)
{
$this->queryToBuild->setQueryDefaultOperator( $operator->getOperator());
return $this;
}
/**
* Remove the default query operator.
*
* @return $this
*/
public function removeOperator()
{
$this->queryToBuild->setQueryDefaultOperator('');
return $this;
}
/**
* @param Slops $slops
* @return $this
*/
public function useSlops(Slops $slops)
{
return $slops->build($this);
}
/**
* Uses the passed boostQuer(y|ies) for the query.
*
* @param string|array $boostQueries
* @return $this
*/
public function useBoostQueries($boostQueries)
{
$boostQueryArray = [];
if(is_array($boostQueries)) {
foreach($boostQueries as $boostQuery) {
$boostQueryArray[] = ['key' => md5($boostQuery), 'query' => $boostQuery];
}
} else {
$boostQueryArray[] = ['key' => md5($boostQueries), 'query' => $boostQueries];
}
$this->queryToBuild->getEDisMax()->setBoostQueries($boostQueryArray);
return $this;
}
/**
* Removes all boost queries from the query.
*
* @return $this
*/
public function removeAllBoostQueries()
{
$this->queryToBuild->getEDisMax()->clearBoostQueries();
return $this;
}
/**
* Uses the passed boostFunction for the query.
*
* @param string $boostFunction
* @return $this
*/
public function useBoostFunction(string $boostFunction)
{
$this->queryToBuild->getEDisMax()->setBoostFunctions($boostFunction);
return $this;
}
/**
* Removes all previously configured boost functions.
*
* @return $this
*/
public function removeAllBoostFunctions()
{
$this->queryToBuild->getEDisMax()->setBoostFunctions('');
return $this;
}
/**
* Uses the passed minimumMatch(mm) for the query.
*
* @param string $minimumMatch
* @return $this
*/
public function useMinimumMatch(string $minimumMatch)
{
$this->queryToBuild->getEDisMax()->setMinimumMatch($minimumMatch);
return $this;
}
/**
* Remove any previous passed minimumMatch parameter.
*
* @return $this
*/
public function removeMinimumMatch()
{
$this->queryToBuild->getEDisMax()->setMinimumMatch('');
return $this;
}
/**
* Applies the tie parameter to the query.
*
* @param mixed $tie
* @return $this
*/
public function useTieParameter($tie)
{
$this->queryToBuild->getEDisMax()->setTie($tie);
return $this;
}
/**
* Applies custom QueryFields to the query.
*
* @param QueryFields $queryFields
* @return $this
*/
public function useQueryFields(QueryFields $queryFields)
{
return $queryFields->build($this);
}
/**
* Applies custom ReturnFields to the query.
*
* @param ReturnFields $returnFields
* @return $this
*/
public function useReturnFields(ReturnFields $returnFields)
{
return $returnFields->build($this);
}
/**
* Can be used to use a specific filter string in the solr query.
*
* @param string $filterString
* @param string $filterName
* @return $this
*/
public function useFilter($filterString, $filterName = '')
{
$filterName = $filterName === '' ? $filterString : $filterName;
$nameWasPassedAndFilterIsAllreadySet = $filterName !== '' && $this->queryToBuild->getFilterQuery($filterName) !== null;
if($nameWasPassedAndFilterIsAllreadySet) {
return $this;
}
$this->queryToBuild->addFilterQuery(['key' => $filterName, 'query' => $filterString]);
return $this;
}
/**
* Removes a filter by the fieldName.
*
* @param string $fieldName
* @return $this
*/
public function removeFilterByFieldName($fieldName)
{
return $this->removeFilterByFunction(
function($key, $query) use ($fieldName) {
$queryString = $query->getQuery();
$storedFieldName = substr($queryString,0, strpos($queryString, ":"));
return $storedFieldName == $fieldName;
}
);
}
/**
* Removes a filter by the name of the filter (also known as key).
*
* @param string $name
* @return $this
*/
public function removeFilterByName($name)
{
return $this->removeFilterByFunction(
function($key, $query) use ($name) {
$key = $query->getKey();
return $key == $name;
}
);
}
/**
* Removes a filter by the filter value.
*
* @param string $value
* @return $this
*/
public function removeFilterByValue($value)
{
return $this->removeFilterByFunction(
function($key, $query) use ($value) {
$query = $query->getQuery();
return $query == $value;
}
);
}
/**
* @param \Closure $filterFunction
* @return $this
*/
public function removeFilterByFunction($filterFunction)
{
$queries = $this->queryToBuild->getFilterQueries();
foreach($queries as $key => $query) {
$canBeRemoved = $filterFunction($key, $query);
if($canBeRemoved) {
unset($queries[$key]);
}
}
$this->queryToBuild->setFilterQueries($queries);
return $this;
}
/**
* Passes the alternative query to the Query
* @param string $query
* @return $this
*/
public function useAlternativeQuery(string $query)
{
$this->queryToBuild->getEDisMax()->setQueryAlternative($query);
return $this;
}
/**
* Remove the alternative query from the Query.
*
* @return $this
*/
public function removeAlternativeQuery()
{
$this->queryToBuild->getEDisMax()->setQueryAlternative(null);
return $this;
}
/**
* Applies a custom Faceting configuration to the query.
*
* @param Faceting $faceting
* @return $this
*/
public function useFaceting(Faceting $faceting)
{
return $faceting->build($this);
}
/**
* @param FieldCollapsing $fieldCollapsing
* @return $this
*/
public function useFieldCollapsing(FieldCollapsing $fieldCollapsing)
{
return $fieldCollapsing->build($this);
}
/**
* Applies a custom initialized grouping to the query.
*
* @param Grouping $grouping
* @return $this
*/
public function useGrouping(Grouping $grouping)
{
return $grouping->build($this);
}
/**
* @param Highlighting $highlighting
* @return $this
*/
public function useHighlighting(Highlighting $highlighting)
{
return $highlighting->build($this);
}
/**
* @param boolean $debugMode
* @return $this
*/
public function useDebug($debugMode)
{
if (!$debugMode) {
$this->queryToBuild->addParam('debugQuery', null);
$this->queryToBuild->addParam('echoParams', null);
return $this;
}
$this->queryToBuild->addParam('debugQuery', 'true');
$this->queryToBuild->addParam('echoParams', 'all');
return $this;
}
/**
* @param Elevation $elevation
* @return QueryBuilder
*/
public function useElevation(Elevation $elevation)
{
return $elevation->build($this);
}
/**
* @param Spellchecking $spellchecking
* @return $this
*/
public function useSpellchecking(Spellchecking $spellchecking)
{
return $spellchecking->build($this);
}
/**
* Applies a custom configured PhraseFields to the query.
*
* @param PhraseFields $phraseFields
* @return $this
*/
public function usePhraseFields(PhraseFields $phraseFields)
{
return $phraseFields->build($this);
}
/**
* Applies a custom configured BigramPhraseFields to the query.
*
* @param BigramPhraseFields $bigramPhraseFields
* @return $this
*/
public function useBigramPhraseFields(BigramPhraseFields $bigramPhraseFields)
{
return $bigramPhraseFields->build($this);
}
/**
* Applies a custom configured TrigramPhraseFields to the query.
*
* @param TrigramPhraseFields $trigramPhraseFields
* @return $this
*/
public function useTrigramPhraseFields(TrigramPhraseFields $trigramPhraseFields)
{
return $trigramPhraseFields->build($this);
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\Query;
/***************************************************************
* Copyright notice
*
* (c) 2010-2015 Ingo Renner <ingo@typo3.org>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project is
* free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use Solarium\QueryType\Extract\Query as SolariumExtractQuery;
/**
* Specialized query for content extraction using Solr Cell
*
*/
class ExtractingQuery extends SolariumExtractQuery
{
/**
* Constructor
*
* @param string $file Absolute path to the file to extract content and meta data from.
*/
public function __construct($file)
{
parent::__construct();
$this->setFile($file);
$this->addParam('extractFormat', 'text');
}
}

View File

@@ -0,0 +1,147 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\Query\Helper;
/***************************************************************
* Copyright notice
*
* (c) 2017 Timo Hund <timo.hund@dkd.de>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project is
* free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
/**
* The EscpaeService is responsible to escape the querystring as expected for Apache Solr.
*
* This class should have no dependencies since it only contains static functions
*
* @author Timo Hund <timo.hund@dkd.de>
*/
class EscapeService {
/**
* Quote and escape search strings
*
* @param string|int|double $string String to escape
* @return string|int|double The escaped/quoted string
*/
public static function escape($string)
{
// when we have a numeric string only, nothing needs to be done
if (is_numeric($string)) {
return $string;
}
// when no whitespaces are in the query we can also just escape the special characters
if (preg_match('/\W/', $string) != 1) {
return static::escapeSpecialCharacters($string);
}
// when there are no quotes inside the query string we can also just escape the whole string
$hasQuotes = strrpos($string, '"') !== false;
if (!$hasQuotes) {
return static::escapeSpecialCharacters($string);
}
$result = static::tokenizeByQuotesAndEscapeDependingOnContext($string);
return $result;
}
/**
* Applies trim and htmlspecialchars on the querystring to use it as output.
*
* @param mixed $string
* @return string
*/
public static function clean($string): string
{
$string = trim($string);
$string = htmlspecialchars($string);
return $string;
}
/**
* This method is used to escape the content in the query string surrounded by quotes
* different then when it is not in a quoted context.
*
* @param string $string
* @return string
*/
protected static function tokenizeByQuotesAndEscapeDependingOnContext($string)
{
$result = '';
$quotesCount = substr_count($string, '"');
$isEvenAmountOfQuotes = $quotesCount % 2 === 0;
// go over all quote segments and apply escapePhrase inside a quoted
// context and escapeSpecialCharacters outside the quoted context.
$segments = explode('"', $string);
$segmentsIndex = 0;
foreach ($segments as $segment) {
$isInQuote = $segmentsIndex % 2 !== 0;
$isLastQuote = $segmentsIndex === $quotesCount;
if ($isLastQuote && !$isEvenAmountOfQuotes) {
$result .= '\"';
}
if ($isInQuote && !$isLastQuote) {
$result .= static::escapePhrase($segment);
} else {
$result .= static::escapeSpecialCharacters($segment);
}
$segmentsIndex++;
}
return $result;
}
/**
* Escapes a value meant to be contained in a phrase with characters with
* special meanings in Lucene query syntax.
*
* @param string $value Unescaped - "dirty" - string
* @return string Escaped - "clean" - string
*/
protected static function escapePhrase($value)
{
$pattern = '/("|\\\)/';
$replace = '\\\$1';
return '"' . preg_replace($pattern, $replace, $value) . '"';
}
/**
* Escapes characters with special meanings in Lucene query syntax.
*
* @param string $value Unescaped - "dirty" - string
* @return string Escaped - "clean" - string
*/
protected static function escapeSpecialCharacters($value)
{
// list taken from http://lucene.apache.org/core/4_4_0/queryparser/org/apache/lucene/queryparser/classic/package-summary.html#package_description
// which mentions: + - && || ! ( ) { } [ ] ^ " ~ * ? : \ /
// of which we escape: ( ) { } [ ] ^ " ~ : \ /
// and explicitly don't escape: + - && || ! * ?
$pattern = '/(\\(|\\)|\\{|\\}|\\[|\\]|\\^|"|~|\:|\\\\|\\/)/';
$replace = '\\\$1';
return preg_replace($pattern, $replace, $value);
}
}

View File

@@ -0,0 +1,46 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\Query\ParameterBuilder;
/***************************************************************
* Copyright notice
*
* (c) 2017 <timo.hund@dkd.de>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project is
* free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
abstract class AbstractDeactivatable {
/**
* @var bool
*/
protected $isEnabled = false;
/**
* @return boolean
*/
public function getIsEnabled()
{
return $this->isEnabled;
}
/**
* @param boolean $isEnabled
*/
public function setIsEnabled($isEnabled)
{
$this->isEnabled = $isEnabled;
}
}

View File

@@ -0,0 +1,115 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\Query\ParameterBuilder;
/***************************************************************
* Copyright notice
*
* (c) 2010-2017 dkd Internet Service GmbH <solr-support@dkd.de>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project is
* free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* The AbstractFieldList class
*/
abstract class AbstractFieldList extends AbstractDeactivatable
{
/**
* @var array
*/
protected $fieldList = [];
/**
* Parameter key which should be used for Apache Solr URL query
*
* @var string
*/
protected $parameterKey = '';
/**
* FieldList parameter builder constructor.
*
* @param array $fieldList
*/
public function __construct($isEnabled, array $fieldList = [])
{
$this->isEnabled = $isEnabled;
$this->fieldList = $fieldList;
}
/**
* @param string $fieldListString
* @param string $delimiter
* @return array
*/
protected static function buildFieldList(string $fieldListString, string $delimiter):array
{
$fields = GeneralUtility::trimExplode($delimiter, $fieldListString, true);
$fieldList = [];
foreach ($fields as $field) {
$fieldNameAndBoost = explode('^', $field);
$boost = 1.0;
if (isset($fieldNameAndBoost[1])) {
$boost = floatval($fieldNameAndBoost[1]);
}
$fieldName = $fieldNameAndBoost[0];
$fieldList[$fieldName] = $boost;
}
return $fieldList;
}
/**
* @param string $fieldName
* @param float $boost
*
* @return AbstractFieldList
*/
public function add(string $fieldName, float $boost = 1.0): AbstractFieldList
{
$this->fieldList[$fieldName] = (float)$boost;
return $this;
}
/**
* Creates the string representation
*
* @param string $delimiter
* @return string
*/
public function toString(string $delimiter = ' ')
{
$fieldListString = '';
foreach ($this->fieldList as $fieldName => $fieldBoost) {
$fieldListString .= $fieldName;
if ($fieldBoost != 1.0) {
$fieldListString .= '^' . number_format($fieldBoost, 1, '.', '');
}
$fieldListString .= $delimiter;
}
return rtrim($fieldListString, $delimiter);
}
}

View File

@@ -0,0 +1,95 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\Query\ParameterBuilder;
/***************************************************************
* Copyright notice
*
* (c) 2010-2017 dkd Internet Service GmbH <solr-support@dkd.de>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project is
* free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use WapplerSystems\Meilisearch\Domain\Search\Query\AbstractQueryBuilder;
use WapplerSystems\Meilisearch\System\Configuration\TypoScriptConfiguration;
/**
* The BigramPhraseFields class
*/
class BigramPhraseFields extends AbstractFieldList implements ParameterBuilder
{
/**
* Parameter key which should be used for Apache Solr URL query
*
* @var string
*/
protected $parameterKey = 'pf2';
/**
* Parses the string representation of the fieldList (e.g. content^100, title^10) to the object representation.
*
* @param string $fieldListString
* @param string $delimiter
* @return BigramPhraseFields
*/
public static function fromString(string $fieldListString, string $delimiter = ',') : BigramPhraseFields
{
return self::initializeFromString($fieldListString, $delimiter);
}
/**
* @param TypoScriptConfiguration $solrConfiguration
* @return BigramPhraseFields
*/
public static function fromTypoScriptConfiguration(TypoScriptConfiguration $solrConfiguration)
{
$isEnabled = $solrConfiguration->getBigramPhraseSearchIsEnabled();
if (!$isEnabled) {
return new BigramPhraseFields(false);
}
return self::fromString((string)$solrConfiguration->getSearchQueryBigramPhraseFields());
}
/**
* Parses the string representation of the fieldList (e.g. content^100, title^10) to the object representation.
*
* @param string $fieldListString
* @param string $delimiter
* @return BigramPhraseFields
*/
protected static function initializeFromString(string $fieldListString, string $delimiter = ',') : BigramPhraseFields
{
$fieldList = self::buildFieldList($fieldListString, $delimiter);
return new BigramPhraseFields(true, $fieldList);
}
/**
* @param AbstractQueryBuilder $parentBuilder
* @return AbstractQueryBuilder
*/
public function build(AbstractQueryBuilder $parentBuilder): AbstractQueryBuilder
{
$bigramPhraseFieldsString = $this->toString();
if ($bigramPhraseFieldsString === '' || !$this->getIsEnabled()) {
return $parentBuilder;
}
$parentBuilder->getQuery()->getEDisMax()->setPhraseBigramFields($bigramPhraseFieldsString);
return $parentBuilder;
}
}

View File

@@ -0,0 +1,139 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\Query\ParameterBuilder;
/***************************************************************
* Copyright notice
*
* (c) 2018 <timo.hund@dkd.de>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project is
* free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use WapplerSystems\Meilisearch\Domain\Search\Query\AbstractQueryBuilder;
use WapplerSystems\Meilisearch\System\Configuration\TypoScriptConfiguration;
/**
* The Elevation ParameterProvider is responsible to build the solr query parameters
* that are needed for the elevation.
*/
class Elevation extends AbstractDeactivatable implements ParameterBuilder
{
/**
* @var bool
*/
protected $isForced = true;
/**
* @var bool
*/
protected $markElevatedResults = true;
/**
* Elevation constructor.
* @param boolean $isEnabled
* @param boolean $isForced
* @param boolean $markElevatedResults
*/
public function __construct($isEnabled = false, $isForced = true, $markElevatedResults = true)
{
$this->isEnabled = $isEnabled;
$this->isForced = $isForced;
$this->markElevatedResults = $markElevatedResults;
}
/**
* @return boolean
*/
public function getIsForced(): bool
{
return $this->isForced;
}
/**
* @param boolean $isForced
*/
public function setIsForced(bool $isForced)
{
$this->isForced = $isForced;
}
/**
* @return boolean
*/
public function getMarkElevatedResults(): bool
{
return $this->markElevatedResults;
}
/**
* @param boolean $markElevatedResults
*/
public function setMarkElevatedResults(bool $markElevatedResults)
{
$this->markElevatedResults = $markElevatedResults;
}
/**
* @param TypoScriptConfiguration $solrConfiguration
* @return Elevation
*/
public static function fromTypoScriptConfiguration(TypoScriptConfiguration $solrConfiguration)
{
$isEnabled = $solrConfiguration->getSearchElevation();
if (!$isEnabled) {
return new Elevation(false);
}
$force = $solrConfiguration->getSearchElevationForceElevation();
$markResults = $solrConfiguration->getSearchElevationMarkElevatedResults();
return new Elevation(true, $force, $markResults);
}
/**
* @return Elevation
*/
public static function getEmpty()
{
return new Elevation(false);
}
/**
* @param AbstractQueryBuilder $parentBuilder
* @return AbstractQueryBuilder
*/
public function build(AbstractQueryBuilder $parentBuilder): AbstractQueryBuilder
{
$query = $parentBuilder->getQuery();
if (!$this->getIsEnabled()) {
$query->addParam('enableElevation', null);
$query->addParam('forceElevation', null);
$query->removeField('isElevated:[elevated]');
return $parentBuilder;
}
$query->addParam('enableElevation', 'true');
$forceElevationString = $this->getIsForced() ? 'true' : 'false';
$query->addParam('forceElevation', $forceElevationString);
if ($this->getMarkElevatedResults()) {
$query->addField('isElevated:[elevated]');
}
return $parentBuilder;
}
}

View File

@@ -0,0 +1,282 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\Query\ParameterBuilder;
/***************************************************************
* Copyright notice
*
* (c) 2017 <timo.hund@dkd.de>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project is
* free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use WapplerSystems\Meilisearch\Domain\Search\Query\AbstractQueryBuilder;
use WapplerSystems\Meilisearch\Domain\Search\Query\QueryBuilder;
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\SortingExpression;
use WapplerSystems\Meilisearch\System\Configuration\TypoScriptConfiguration;
/**
* The Faceting ParameterProvider is responsible to build the solr query parameters
* that are needed for the highlighting.
*/
class Faceting extends AbstractDeactivatable implements ParameterBuilder
{
/**
* @var string
*/
protected $sorting = '';
/**
* @var int
*/
protected $minCount = 1;
/**
* @var int
*/
protected $limit = 10;
/**
* @var array
*/
protected $fields = [];
/**
* @var array
*/
protected $additionalParameters = [];
/**
* Faceting constructor.
*
* @param bool $isEnabled
* @param string $sorting
* @param int $minCount
* @param int $limit
* @param array $fields
* @param array $additionalParameters
*/
public function __construct($isEnabled, $sorting = '', $minCount = 1, $limit = 10, $fields = [], $additionalParameters = [])
{
$this->isEnabled = $isEnabled;
$this->sorting = $sorting;
$this->minCount = $minCount;
$this->limit = $limit;
$this->fields = $fields;
$this->additionalParameters = $additionalParameters;
}
/**
* @return string
*/
public function getSorting()
{
return $this->sorting;
}
/**
* @param string $sorting
*/
public function setSorting($sorting)
{
$this->sorting = $sorting;
}
/**
* @return int
*/
public function getMinCount()
{
return $this->minCount;
}
/**
* @param int $minCount
*/
public function setMinCount($minCount)
{
$this->minCount = $minCount;
}
/**
* @return mixed
*/
public function getLimit()
{
return $this->limit;
}
/**
* @param mixed $limit
*/
public function setLimit($limit)
{
$this->limit = $limit;
}
/**
* @return array
*/
public function getFields()
{
return $this->fields;
}
/**
* @param array $fields
*/
public function setFields(array $fields)
{
$this->fields = $fields;
}
/**
* @param string $fieldName
*/
public function addField($fieldName)
{
$this->fields[] = $fieldName;
}
/**
* @return array
*/
public function getAdditionalParameters(): array
{
return $this->additionalParameters;
}
/**
* @param array $additionalParameters
*/
public function setAdditionalParameters(array $additionalParameters)
{
$this->additionalParameters = $additionalParameters;
}
/**
* @param array $value
*/
public function addAdditionalParameter($key, $value)
{
$this->additionalParameters[$key] = $value;
}
/**
* Reads the facet sorting configuration and applies it to the queryParameters.
*
* @param array $facetParameters
* @return array
*/
protected function applySorting(array $facetParameters)
{
$sortingExpression = new SortingExpression();
$globalSortingExpression = $sortingExpression->getForFacet($this->sorting);
if (!empty($globalSortingExpression)) {
$facetParameters['facet.sort'] = $globalSortingExpression;
}
return $facetParameters;
}
/**
* @param TypoScriptConfiguration $solrConfiguration
* @return Faceting
*/
public static function fromTypoScriptConfiguration(TypoScriptConfiguration $solrConfiguration)
{
$isEnabled = $solrConfiguration->getSearchFaceting();
if (!$isEnabled) {
return new Faceting(false);
}
$minCount = $solrConfiguration->getSearchFacetingMinimumCount();
$limit = $solrConfiguration->getSearchFacetingFacetLimit();
$sorting = $solrConfiguration->getSearchFacetingSortBy();
return new Faceting($isEnabled, $sorting, $minCount, $limit);
}
/**
* @return Faceting
*/
public static function getEmpty()
{
return new Faceting(false);
}
/**
* Retrieves all parameters that are required for faceting.
*
* @return array
*/
protected function getFacetParameters() {
$facetParameters = [];
$facetParameters['facet'] = 'true';
$facetParameters['facet.mincount'] = $this->getMinCount();
$facetParameters['facet.limit'] = $this->getLimit();
$facetParameters['facet.field'] = $this->getFields();
foreach ($this->getAdditionalParameters() as $additionalParameterKey => $additionalParameterValue) {
$facetParameters[$additionalParameterKey] = $additionalParameterValue;
}
if ($facetParameters['json.facet']) {
$facetParameters['json.facet'] = json_encode($facetParameters['json.facet']);
}
$facetParameters = $this->applySorting($facetParameters);
return $facetParameters;
}
/**
* @param AbstractQueryBuilder $parentBuilder
* @return QueryBuilder
*/
public function build(AbstractQueryBuilder $parentBuilder): AbstractQueryBuilder
{
$query = $parentBuilder->getQuery();
if (!$this->getIsEnabled()) {
//@todo use unset functionality when present
$query->addParam('facet', null);
$query->addParam('lex', null);
$query->addParam('json.mincount', null);
$query->addParam('json.limit', null);
$query->addParam('json.field', null);
$query->addParam('facet.sort', null);
$params = $query->getParams();
foreach($params as $key => $value) {
if (strpos($key, 'f.') !== false) {
$query->addParam($key, null);
}
}
return $parentBuilder;
}
//@todo check of $this->queryToBuilder->getFacetSet() can be used
$facetingParameters = $this->getFacetParameters();
foreach($facetingParameters as $key => $value) {
$query->addParam($key, $value);
}
return $parentBuilder;
}
}

View File

@@ -0,0 +1,159 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\Query\ParameterBuilder;
/***************************************************************
* Copyright notice
*
* (c) 2017 <timo.hund@dkd.de>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project is
* free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use WapplerSystems\Meilisearch\Domain\Search\Query\AbstractQueryBuilder;
use WapplerSystems\Meilisearch\System\Configuration\TypoScriptConfiguration;
/**
* The FieldCollapsing ParameterProvider is responsible to build the solr query parameters
* that are needed for the field collapsing.
*/
class FieldCollapsing extends AbstractDeactivatable implements ParameterBuilder
{
/**
* @var string
*/
protected $collapseFieldName = 'variantId';
/**
* @var bool
*/
protected $expand = false;
/**
* @var int
*/
protected $expandRowCount = 10;
/**
* FieldCollapsing constructor.
* @param bool $isEnabled
* @param string $collapseFieldName
* @param bool $expand
* @param int $expandRowCount
*/
public function __construct($isEnabled, $collapseFieldName = 'variantId', $expand = false, $expandRowCount = 10)
{
$this->isEnabled = $isEnabled;
$this->collapseFieldName = $collapseFieldName;
$this->expand = $expand;
$this->expandRowCount = $expandRowCount;
}
/**
* @return string
*/
public function getCollapseFieldName(): string
{
return $this->collapseFieldName;
}
/**
* @param string $collapseFieldName
*/
public function setCollapseFieldName(string $collapseFieldName)
{
$this->collapseFieldName = $collapseFieldName;
}
/**
* @return boolean
*/
public function getIsExpand(): bool
{
return $this->expand;
}
/**
* @param boolean $expand
*/
public function setExpand(bool $expand)
{
$this->expand = $expand;
}
/**
* @return int
*/
public function getExpandRowCount(): int
{
return $this->expandRowCount;
}
/**
* @param int $expandRowCount
*/
public function setExpandRowCount(int $expandRowCount)
{
$this->expandRowCount = $expandRowCount;
}
/**
* @param TypoScriptConfiguration $solrConfiguration
* @return FieldCollapsing
*/
public static function fromTypoScriptConfiguration(TypoScriptConfiguration $solrConfiguration)
{
$isEnabled = $solrConfiguration->getSearchVariants();
if (!$isEnabled) {
return new FieldCollapsing(false);
}
$collapseField = $solrConfiguration->getSearchVariantsField();
$expand = (bool)$solrConfiguration->getSearchVariantsExpand();
$expandRows = $solrConfiguration->getSearchVariantsLimit();
return new FieldCollapsing(true, $collapseField, $expand, $expandRows);
}
/**
* @return FieldCollapsing
*/
public static function getEmpty()
{
return new FieldCollapsing(false);
}
/**
* @param AbstractQueryBuilder $parentBuilder
* @return AbstractQueryBuilder
*/
public function build(AbstractQueryBuilder $parentBuilder): AbstractQueryBuilder
{
$query = $parentBuilder->getQuery();
if(!$this->getIsEnabled()) {
return $parentBuilder;
}
$parentBuilder->useFilter('{!collapse field=' . $this->getCollapseFieldName(). '}', 'fieldCollapsing');
if($this->getIsExpand()) {
$query->addParam('expand', 'true');
$query->addParam('expand.rows', $this->getExpandRowCount());
}
return $parentBuilder;
}
}

View File

@@ -0,0 +1,158 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\Query\ParameterBuilder;
/***************************************************************
* Copyright notice
*
* (c) 2017 <timo.hund@dkd.de>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project is
* free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use WapplerSystems\Meilisearch\System\Configuration\TypoScriptConfiguration;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* The Filters ParameterProvider is responsible to build the solr query parameters
* that are needed for the filtering.
*/
class Filters
{
/**
* @var array
*/
protected $filters = [];
/**
* Removes a filter on a field
*
* @param string $filterFieldName The field name the filter should be removed for
* @return void
*/
public function removeByFieldName($filterFieldName)
{
$this->removeByPrefix($filterFieldName . ':');
}
/**
* @param string $filterFieldName
*/
public function removeByPrefix($filterFieldName)
{
foreach ($this->filters as $key => $filterString) {
if (GeneralUtility::isFirstPartOfStr($filterString, $filterFieldName)) {
unset($this->filters[$key]);
}
}
}
/**
* Removes a filter based on name of filter array
*
* @param string $name name of the filter
*/
public function removeByName($name)
{
unset($this->filters[$name]);
}
/**
* @param string $filterString
* @param string $name
*/
public function add($filterString, $name = '')
{
if ($name !== '') {
$this->filters[$name] = $filterString;
} else {
$this->filters[] = $filterString;
}
}
/**
* Add's multiple filters to the filter collection.
*
* @param array $filterArray
* @return Filters
*/
public function addMultiple($filterArray)
{
foreach($filterArray as $key => $value) {
if (!$this->hasWithName($key)) {
$this->add($value, $key);
}
}
return $this;
}
/**
* @param string $name
* @return bool
*/
public function hasWithName($name)
{
return array_key_exists($name, $this->filters);
}
/**
* Removes a filter by the filter value. The value has the following format:
*
* "fieldname:value"
*
* @param string $filterString The filter to remove, in the form of field:value
*/
public function removeByValue($filterString)
{
$key = array_search($filterString, $this->filters);
if ($key === false) {
// value not found, nothing to do
return;
}
unset($this->filters[$key]);
}
/**
* Gets all currently applied filters.
*
* @return array Array of filters
*/
public function getValues()
{
return $this->filters;
}
/**
* @param TypoScriptConfiguration $solrConfiguration
* @return Filters
*/
public static function fromTypoScriptConfiguration(TypoScriptConfiguration $solrConfiguration)
{
return new Filters();
}
/**
* @return Filters
*/
public static function getEmpty()
{
return new Filters();
}
}

View File

@@ -0,0 +1,254 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\Query\ParameterBuilder;
/***************************************************************
* Copyright notice
*
* (c) 2017 <timo.hund@dkd.de>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project is
* free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use WapplerSystems\Meilisearch\Domain\Search\Query\AbstractQueryBuilder;
use WapplerSystems\Meilisearch\System\Configuration\TypoScriptConfiguration;
/**
* The Grouping ParameterProvider is responsible to build the solr query parameters
* that are needed for the grouping.
*/
class Grouping extends AbstractDeactivatable implements ParameterBuilder
{
/**
* @var array
*/
protected $fields = [];
/**
* @var array
*/
protected $sortings = [];
/**
* @var array
*/
protected $queries = [];
/**
* @var int
*/
protected $numberOfGroups = 5;
/**
* @var int
*/
protected $resultsPerGroup = 1;
/**
* Grouping constructor.
*
* @param bool $isEnabled
* @param array $fields
* @param array $sortings
* @param array $queries
* @param int $numberOfGroups
* @param int $resultsPerGroup
*/
public function __construct($isEnabled, array $fields = [], array $sortings = [], array $queries = [], $numberOfGroups = 5, $resultsPerGroup = 1)
{
$this->isEnabled = $isEnabled;
$this->fields = $fields;
$this->sortings = $sortings;
$this->queries = $queries;
$this->numberOfGroups = $numberOfGroups;
$this->resultsPerGroup = $resultsPerGroup;
}
/**
* @return array
*/
public function getFields()
{
return $this->fields;
}
/**
* @param array $fields
*/
public function setFields(array $fields)
{
$this->fields = $fields;
}
/**
* @param string $field
*/
public function addField(string $field)
{
$this->fields[] = $field;
}
/**
* @return array
*/
public function getSortings()
{
return $this->sortings;
}
/**
* @param string $sorting
*/
public function addSorting($sorting)
{
$this->sortings[] = $sorting;
}
/**
* @param array $sortings
*/
public function setSortings(array $sortings)
{
$this->sortings = $sortings;
}
/**
* @return array
*/
public function getQueries(): array
{
return $this->queries;
}
/**
* @param string $query
*/
public function addQuery($query)
{
$this->queries[] = $query;
}
/**
* @param array $queries
*/
public function setQueries(array $queries)
{
$this->queries = $queries;
}
/**
* @return int
*/
public function getNumberOfGroups()
{
return $this->numberOfGroups;
}
/**
* @param int $numberOfGroups
*/
public function setNumberOfGroups($numberOfGroups)
{
$this->numberOfGroups = $numberOfGroups;
}
/**
* @return int
*/
public function getResultsPerGroup()
{
return $this->resultsPerGroup;
}
/**
* @param int $resultsPerGroup
*/
public function setResultsPerGroup($resultsPerGroup)
{
$resultsPerGroup = max(intval($resultsPerGroup), 0);
$this->resultsPerGroup = $resultsPerGroup;
}
/**
* @param TypoScriptConfiguration $solrConfiguration
* @return Grouping
*/
public static function fromTypoScriptConfiguration(TypoScriptConfiguration $solrConfiguration)
{
$isEnabled = $solrConfiguration->getSearchGrouping();
if (!$isEnabled) {
return new Grouping(false);
}
$fields = [];
$queries = [];
$sortings = [];
$resultsPerGroup = $solrConfiguration->getSearchGroupingHighestGroupResultsLimit();
$configuredGroups = $solrConfiguration->getSearchGroupingGroupsConfiguration();
$numberOfGroups = $solrConfiguration->getSearchGroupingNumberOfGroups();
$sortBy = $solrConfiguration->getSearchGroupingSortBy();
foreach ($configuredGroups as $groupName => $groupConfiguration) {
if (isset($groupConfiguration['field'])) {
$fields[] = $groupConfiguration['field'];
} elseif (isset($groupConfiguration['query'])) {
$queries[] = $groupConfiguration['query'];
}
}
if (!empty(trim($sortBy))) {
$sortings[] = $sortBy;
}
return new Grouping($isEnabled, $fields, $sortings, $queries, $numberOfGroups, $resultsPerGroup);
}
/**
* @return Grouping
*/
public static function getEmpty()
{
return new Grouping(false);
}
/**
* @param AbstractQueryBuilder $parentBuilder
* @return AbstractQueryBuilder
*/
public function build(AbstractQueryBuilder $parentBuilder): AbstractQueryBuilder
{
$query = $parentBuilder->getQuery();
if(!$this->getIsEnabled()) {
$query->removeComponent($query->getGrouping());
return $parentBuilder;
}
$query->getGrouping()->setFields($this->getFields());
$query->getGrouping()->setLimit($this->getResultsPerGroup());
$query->getGrouping()->setQueries($this->getQueries());
$query->getGrouping()->setFormat('grouped');
$query->getGrouping()->setNumberOfGroups(true);
$query->setRows($this->getNumberOfGroups());
$sorting = implode(' ', $this->getSortings());
$query->getGrouping()->setSort($sorting);
return $parentBuilder;
}
}

View File

@@ -0,0 +1,211 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\Query\ParameterBuilder;
/***************************************************************
* Copyright notice
*
* (c) 2017 <timo.hund@dkd.de>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project is
* free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use WapplerSystems\Meilisearch\Domain\Search\Query\AbstractQueryBuilder;
use WapplerSystems\Meilisearch\Domain\Search\Query\QueryBuilder;
use WapplerSystems\Meilisearch\System\Configuration\TypoScriptConfiguration;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* The Highlighting ParameterProvider is responsible to build the solr query parameters
* that are needed for the highlighting.
*/
class Highlighting extends AbstractDeactivatable implements ParameterBuilder
{
/**
* @var int
*/
protected $fragmentSize = 200;
/**
* @var string
*/
protected $highlightingFieldList = '';
/**
* @var string
*/
protected $prefix = '';
/**
* @var string
*/
protected $postfix = '';
/**
* Highlighting constructor.
*
* @param bool $isEnabled
* @param int $fragmentSize
* @param string $highlightingFieldList
* @param string $prefix
* @param string $postfix
*/
public function __construct($isEnabled = false, $fragmentSize = 200, $highlightingFieldList = '', $prefix = '', $postfix = '')
{
$this->isEnabled = $isEnabled;
$this->fragmentSize = $fragmentSize;
$this->highlightingFieldList = $highlightingFieldList;
$this->prefix = $prefix;
$this->postfix = $postfix;
}
/**
* @return int
*/
public function getFragmentSize(): int
{
return $this->fragmentSize;
}
/**
* @param int $fragmentSize
*/
public function setFragmentSize(int $fragmentSize)
{
$this->fragmentSize = $fragmentSize;
}
/**
* @return string
*/
public function getHighlightingFieldList(): string
{
return $this->highlightingFieldList;
}
/**
* @param string $highlightingFieldList
*/
public function setHighlightingFieldList(string $highlightingFieldList)
{
$this->highlightingFieldList = $highlightingFieldList;
}
/**
* @return string
*/
public function getPrefix(): string
{
return $this->prefix;
}
/**
* @param string $prefix
*/
public function setPrefix(string $prefix)
{
$this->prefix = $prefix;
}
/**
* @return string
*/
public function getPostfix(): string
{
return $this->postfix;
}
/**
* @param string $postfix
*/
public function setPostfix(string $postfix)
{
$this->postfix = $postfix;
}
/**
* @return bool
*/
public function getUseFastVectorHighlighter()
{
return ($this->fragmentSize >= 18);
}
/**
* @param TypoScriptConfiguration $solrConfiguration
* @return Highlighting
*/
public static function fromTypoScriptConfiguration(TypoScriptConfiguration $solrConfiguration)
{
$isEnabled = $solrConfiguration->getSearchResultsHighlighting();
if (!$isEnabled) {
return new Highlighting(false);
}
$fragmentSize = $solrConfiguration->getSearchResultsHighlightingFragmentSize();
$highlightingFields = $solrConfiguration->getSearchResultsHighlightingFields();
$wrap = explode('|', $solrConfiguration->getSearchResultsHighlightingWrap());
$prefix = isset($wrap[0]) ? $wrap[0] : '';
$postfix = isset($wrap[1]) ? $wrap[1] : '';
return new Highlighting($isEnabled, $fragmentSize, $highlightingFields, $prefix, $postfix);
}
/**
* @return Highlighting
*/
public static function getEmpty()
{
return new Highlighting(false);
}
/**
* @param AbstractQueryBuilder $parentBuilder
* @return AbstractQueryBuilder
*/
public function build(AbstractQueryBuilder $parentBuilder): AbstractQueryBuilder
{
$query = $parentBuilder->getQuery();
if(!$this->getIsEnabled()) {
$query->removeComponent($query->getHighlighting());
return $parentBuilder;
}
$query->getHighlighting()->setFragSize($this->getFragmentSize());
$query->getHighlighting()->setFields(GeneralUtility::trimExplode(",", $this->getHighlightingFieldList()));
if ($this->getUseFastVectorHighlighter()) {
$query->getHighlighting()->setUseFastVectorHighlighter(true);
$query->getHighlighting()->setTagPrefix($this->getPrefix());
$query->getHighlighting()->setTagPostfix($this->getPostfix());
} else {
$query->getHighlighting()->setUseFastVectorHighlighter(false);
$query->getHighlighting()->setTagPrefix('');
$query->getHighlighting()->setTagPostfix('');
}
if ($this->getPrefix() !== '' && $this->getPostfix() !== '') {
$query->getHighlighting()->setSimplePrefix($this->getPrefix());
$query->getHighlighting()->setSimplePostfix($this->getPostfix());
}
return $parentBuilder;
}
}

View File

@@ -0,0 +1,106 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\Query\ParameterBuilder;
/***************************************************************
* Copyright notice
*
* (c) 2017 <timo.hund@dkd.de>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project is
* free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
/**
* The Operator ParameterProvider is responsible to build the solr query parameters
* that are needed for the operator q.op.
*/
class Operator extends AbstractDeactivatable
{
const OPERATOR_AND = 'AND';
const OPERATOR_OR = 'OR';
/**
* @var string
*/
protected $operator = 'AND';
/**
* Faceting constructor.
*
* @param bool $isEnabled
* @param string $operator
*/
public function __construct($isEnabled, $operator = Operator::OPERATOR_AND)
{
$this->isEnabled = $isEnabled;
$this->setOperator($operator);
}
/**
* @param string $operator
*/
public function setOperator($operator)
{
if (!in_array($operator, [self::OPERATOR_AND, self::OPERATOR_OR])) {
throw new \InvalidArgumentException("Invalid operator");
}
$this->operator = $operator;
}
/**
* @return string
*/
public function getOperator(): string
{
return $this->operator;
}
/**
* @return Operator
*/
public static function getEmpty(): Operator
{
return new Operator(false);
}
/**
* @return Operator
*/
public static function getAnd(): Operator
{
return new Operator(true, static::OPERATOR_AND);
}
/**
* @return Operator
*/
public static function getOr(): Operator
{
return new Operator(true, static::OPERATOR_OR);
}
/**
* @param string $operator
* @return Operator
*/
public static function fromString($operator)
{
return new Operator(true, $operator);
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\Query\ParameterBuilder;
/***************************************************************
* Copyright notice
*
* (c) 2017 <timo.hund@dkd.de>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project is
* free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use WapplerSystems\Meilisearch\Domain\Search\Query\AbstractQueryBuilder;
/**
* The implementation of ParameterBuilder is responsible to build an array with
* the query parameter that are needed for solr
*
* Interface ParameterProvider
*/
interface ParameterBuilder {
/**
* @param AbstractQueryBuilder $parentBuilder
* @return AbstractQueryBuilder
*/
public function build(AbstractQueryBuilder $parentBuilder): AbstractQueryBuilder;
}

View File

@@ -0,0 +1,95 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\Query\ParameterBuilder;
/***************************************************************
* Copyright notice
*
* (c) 2010-2017 dkd Internet Service GmbH <solr-support@dkd.de>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project is
* free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use WapplerSystems\Meilisearch\Domain\Search\Query\AbstractQueryBuilder;
use WapplerSystems\Meilisearch\System\Configuration\TypoScriptConfiguration;
/**
* The PhraseFields class
*/
class PhraseFields extends AbstractFieldList implements ParameterBuilder
{
/**
* Parameter key which should be used for Apache Solr URL query
*
* @var string
*/
protected $parameterKey = 'pf';
/**
* Parses the string representation of the fieldList (e.g. content^100, title^10) to the object representation.
*
* @param string $fieldListString
* @param string $delimiter
* @return PhraseFields
*/
public static function fromString(string $fieldListString, string $delimiter = ',') : PhraseFields
{
return self::initializeFromString($fieldListString, $delimiter);
}
/**
* @param TypoScriptConfiguration $solrConfiguration
* @return PhraseFields
*/
public static function fromTypoScriptConfiguration(TypoScriptConfiguration $solrConfiguration)
{
$isEnabled = $solrConfiguration->getPhraseSearchIsEnabled();
if (!$isEnabled) {
return new PhraseFields(false);
}
return self::fromString((string)$solrConfiguration->getSearchQueryPhraseFields());
}
/**
* Parses the string representation of the fieldList (e.g. content^100, title^10) to the object representation.
*
* @param string $fieldListString
* @param string $delimiter
* @return PhraseFields
*/
protected static function initializeFromString(string $fieldListString, string $delimiter = ',') : PhraseFields
{
$fieldList = self::buildFieldList($fieldListString, $delimiter);
return new PhraseFields(true, $fieldList);
}
/**
* @param AbstractQueryBuilder $parentBuilder
* @return AbstractQueryBuilder
*/
public function build(AbstractQueryBuilder $parentBuilder): AbstractQueryBuilder
{
$phraseFieldString = $this->toString();
if ($phraseFieldString === '' || !$this->getIsEnabled()) {
return $parentBuilder;
}
$parentBuilder->getQuery()->getEDisMax()->setPhraseFields($phraseFieldString);
return $parentBuilder;
}
}

View File

@@ -0,0 +1,117 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\Query\ParameterBuilder;
/***************************************************************
* Copyright notice
*
* (c) 2017 <timo.hund@dkd.de>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project is
* free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use WapplerSystems\Meilisearch\Domain\Search\Query\AbstractQueryBuilder;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* The QueryFields class holds all information for the query which fields should be used to query (Solr qf parameter).
*/
class QueryFields implements ParameterBuilder
{
/**
* @var array
*/
protected $queryFields = [];
/**
* QueryFields constructor.
*
* @param array $queryFields
*/
public function __construct(array $queryFields = [])
{
$this->queryFields = $queryFields;
}
/**
* @param string $fieldName
* @param float $boost
*/
public function set($fieldName, $boost = 1.0)
{
$this->queryFields[$fieldName] = (float)$boost;
}
/**
* Creates the string representation
*
* @param string $delimiter
* @return string
*/
public function toString($delimiter = ' ') {
$queryFieldString = '';
foreach ($this->queryFields as $fieldName => $fieldBoost) {
$queryFieldString .= $fieldName;
if ($fieldBoost != 1.0) {
$queryFieldString .= '^' . number_format($fieldBoost, 1, '.', '');
}
$queryFieldString .= $delimiter;
}
return rtrim($queryFieldString, $delimiter);
}
/**
* Parses the string representation of the queryFields (e.g. content^100, title^10) to the object representation.
*
* @param string $queryFieldsString
* @param string $delimiter
* @return QueryFields
*/
public static function fromString($queryFieldsString, $delimiter = ',') {
$fields = GeneralUtility::trimExplode($delimiter, $queryFieldsString, true);
$queryFields = [];
foreach ($fields as $field) {
$fieldNameAndBoost = explode('^', $field);
$boost = 1.0;
if (isset($fieldNameAndBoost[1])) {
$boost = floatval($fieldNameAndBoost[1]);
}
$fieldName = $fieldNameAndBoost[0];
$queryFields[$fieldName] = $boost;
}
return new QueryFields($queryFields);
}
/**
* @param AbstractQueryBuilder $parentBuilder
* @return AbstractQueryBuilder
*/
public function build(AbstractQueryBuilder $parentBuilder): AbstractQueryBuilder
{
$parentBuilder->getQuery()->getEDisMax()->setQueryFields($this->toString());
return $parentBuilder;
}
}

View File

@@ -0,0 +1,128 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\Query\ParameterBuilder;
/***************************************************************
* Copyright notice
*
* (c) 2017 <timo.hund@dkd.de>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project is
* free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use WapplerSystems\Meilisearch\Domain\Search\Query\AbstractQueryBuilder;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* The ReturnFields class is responsible to hold a list of field names that should be returned from
* solr.
*/
class ReturnFields implements ParameterBuilder
{
/**
* @var array
*/
protected $fieldList = [];
/**
* FieldList constructor.
*
* @param array $fieldList
*/
public function __construct(array $fieldList = [])
{
$this->fieldList = $fieldList;
}
/**
* Adds a field to the list of fields to return. Also checks whether * is
* set for the fields, if so it's removed from the field list.
*
* @param string $fieldName Name of a field to return in the result documents
*/
public function add($fieldName)
{
if (strpos($fieldName, '[') === false && strpos($fieldName, ']') === false && in_array('*', $this->fieldList)) {
$this->fieldList = array_diff($this->fieldList, ['*']);
}
$this->fieldList[] = $fieldName;
}
/**
* Removes a field from the list of fields to return (fl parameter).
*
* @param string $fieldName Field to remove from the list of fields to return
*/
public function remove($fieldName)
{
$key = array_search($fieldName, $this->fieldList);
if ($key !== false) {
unset($this->fieldList[$key]);
}
}
/**
* @param string $delimiter
* @return string
*/
public function toString($delimiter = ',')
{
return implode($delimiter, $this->fieldList);
}
/**
* @param string $fieldList
* @param string $delimiter
* @return ReturnFields
*/
public static function fromString($fieldList, $delimiter = ',')
{
$fieldListArray = GeneralUtility::trimExplode($delimiter, $fieldList);
return static::fromArray($fieldListArray);
}
/**
* @param array $fieldListArray
* @return ReturnFields
*/
public static function fromArray(array $fieldListArray)
{
return new ReturnFields($fieldListArray);
}
/**
* @return array
*/
public function getValues()
{
return array_unique(array_values($this->fieldList));
}
/**
* @param AbstractQueryBuilder $parentBuilder
* @return AbstractQueryBuilder
*/
public function build(AbstractQueryBuilder $parentBuilder): AbstractQueryBuilder
{
$parentBuilder->getQuery()->setFields($this->getValues());
return $parentBuilder;
}
}

View File

@@ -0,0 +1,257 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\Query\ParameterBuilder;
/***************************************************************
* Copyright notice
*
* (c) 2017 <timo.hund@dkd.de>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project is
* free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use WapplerSystems\Meilisearch\Domain\Search\Query\AbstractQueryBuilder;
use WapplerSystems\Meilisearch\System\Configuration\TypoScriptConfiguration;
/**
* The Slops ParameterProvider is responsible to build the solr query parameters
* that are needed for the several slop arguments.
*/
class Slops implements ParameterBuilder
{
const NO_SLOP = null;
/**
* The qs parameter
*
* @var int
*/
protected $querySlop = self::NO_SLOP;
/**
* @var int
*/
protected $phraseSlop = self::NO_SLOP;
/**
* @var int
*/
protected $bigramPhraseSlop = self::NO_SLOP;
/**
* @var int
*/
protected $trigramPhraseSlop = self::NO_SLOP;
/**
* Slops constructor.
* @param int|null $querySlop
* @param int|null $phraseSlop
* @param int|null $bigramPhraseSlop
* @param int|null $trigramPhraseSlop
*/
public function __construct($querySlop = self::NO_SLOP, $phraseSlop = self::NO_SLOP, $bigramPhraseSlop = self::NO_SLOP, $trigramPhraseSlop = self::NO_SLOP)
{
$this->querySlop = $querySlop;
$this->phraseSlop = $phraseSlop;
$this->bigramPhraseSlop = $bigramPhraseSlop;
$this->trigramPhraseSlop = $trigramPhraseSlop;
}
/**
* @return boolean
*/
public function getHasQuerySlop()
{
return $this->querySlop !== null;
}
/**
* @return int|null
*/
public function getQuerySlop()
{
return $this->querySlop;
}
/**
* @param int $querySlop
*/
public function setQuerySlop(int $querySlop)
{
$this->querySlop = $querySlop;
}
/**
* @return boolean
*/
public function getHasPhraseSlop()
{
return $this->phraseSlop !== null;
}
/**
* @return int|null
*/
public function getPhraseSlop()
{
return $this->phraseSlop;
}
/**
* @param int $phraseSlop
*/
public function setPhraseSlop(int $phraseSlop)
{
$this->phraseSlop = $phraseSlop;
}
/**
* @return boolean
*/
public function getHasBigramPhraseSlop()
{
return $this->bigramPhraseSlop !== null;
}
/**
* @return int|null
*/
public function getBigramPhraseSlop()
{
return $this->bigramPhraseSlop;
}
/**
* @param int $bigramPhraseSlop
*/
public function setBigramPhraseSlop(int $bigramPhraseSlop)
{
$this->bigramPhraseSlop = $bigramPhraseSlop;
}
/**
* @return boolean
*/
public function getHasTrigramPhraseSlop()
{
return $this->trigramPhraseSlop !== null;
}
/**
* @return int|null
*/
public function getTrigramPhraseSlop()
{
return $this->trigramPhraseSlop;
}
/**
* @param int $trigramPhraseSlop
*/
public function setTrigramPhraseSlop(int $trigramPhraseSlop)
{
$this->trigramPhraseSlop = $trigramPhraseSlop;
}
/**
* @param TypoScriptConfiguration $solrConfiguration
* @return Slops
*/
public static function fromTypoScriptConfiguration(TypoScriptConfiguration $solrConfiguration)
{
$searchConfiguration = $solrConfiguration->getSearchConfiguration();
$querySlop = static::getQuerySlopFromConfiguration($searchConfiguration);
$phraseSlop = static::getPhraseSlopFromConfiguration($searchConfiguration);
$bigramPhraseSlop = static::getBigramPhraseSlopFromConfiguration($searchConfiguration);
$trigramPhraseSlop = static::getTrigramPhraseSlopFromConfiguration($searchConfiguration);
return new Slops($querySlop, $phraseSlop, $bigramPhraseSlop, $trigramPhraseSlop);
}
/**
* @param array $searchConfiguration
* @return int|null
*/
protected static function getPhraseSlopFromConfiguration($searchConfiguration)
{
$phraseEnabled = !(empty($searchConfiguration['query.']['phrase']) || $searchConfiguration['query.']['phrase'] !== 1);
$phraseSlopConfigured = !empty($searchConfiguration['query.']['phrase.']['slop']);
return ($phraseEnabled && $phraseSlopConfigured) ? $searchConfiguration['query.']['phrase.']['slop'] : self::NO_SLOP;
}
/**
* @param array $searchConfiguration
* @return int|null
*/
protected static function getQuerySlopFromConfiguration($searchConfiguration)
{
$phraseEnabled = !(empty($searchConfiguration['query.']['phrase']) || $searchConfiguration['query.']['phrase'] !== 1);
$querySlopConfigured = !empty($searchConfiguration['query.']['phrase.']['querySlop']);
return ($phraseEnabled && $querySlopConfigured) ? $searchConfiguration['query.']['phrase.']['querySlop'] : self::NO_SLOP;
}
/**
* @param array $searchConfiguration
* @return int|null
*/
protected static function getBigramPhraseSlopFromConfiguration($searchConfiguration)
{
$bigramPhraseEnabled = !empty($searchConfiguration['query.']['bigramPhrase']) && $searchConfiguration['query.']['bigramPhrase'] === 1;
$bigramSlopConfigured = !empty($searchConfiguration['query.']['bigramPhrase.']['slop']);
return ($bigramPhraseEnabled && $bigramSlopConfigured) ? $searchConfiguration['query.']['bigramPhrase.']['slop'] : self::NO_SLOP;
}
/**
* @param array $searchConfiguration
* @return int|null
*/
protected static function getTrigramPhraseSlopFromConfiguration($searchConfiguration)
{
$trigramPhraseEnabled = !empty($searchConfiguration['query.']['trigramPhrase']) && $searchConfiguration['query.']['trigramPhrase'] === 1;
$trigramSlopConfigured = !empty($searchConfiguration['query.']['trigramPhrase.']['slop']);
return ($trigramPhraseEnabled && $trigramSlopConfigured) ? $searchConfiguration['query.']['trigramPhrase.']['slop'] : self::NO_SLOP;
}
/**
* @param AbstractQueryBuilder $parentBuilder
* @return AbstractQueryBuilder
*/
public function build(AbstractQueryBuilder $parentBuilder): AbstractQueryBuilder
{
$query = $parentBuilder->getQuery();
if ($this->getHasPhraseSlop()) {
$query->getEDisMax()->setPhraseSlop($this->getPhraseSlop());
}
if ($this->getHasBigramPhraseSlop()) {
$query->getEDisMax()->setPhraseBigramSlop($this->getBigramPhraseSlop());
}
if ($this->getHasTrigramPhraseSlop()) {
$query->getEDisMax()->setPhraseTrigramSlop($this->getTrigramPhraseSlop());
}
if ($this->getHasQuerySlop()) {
$query->getEDisMax()->setQueryPhraseSlop($this->getQuerySlop());
}
return $parentBuilder;
}
}

View File

@@ -0,0 +1,112 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\Query\ParameterBuilder;
/***************************************************************
* Copyright notice
*
* (c) 2017 <timo.hund@dkd.de>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project is
* free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* The Sorting ParameterProvider is responsible to build the solr query parameters
* that are needed for the sorting.
*/
class Sorting extends AbstractDeactivatable
{
const SORT_ASC = 'ASC';
const SORT_DESC = 'DESC';
/**
* @var string
*/
protected $fieldName = '';
/**
* @var string
*/
protected $direction = self::SORT_ASC;
/**
* Debug constructor.
*
* @param bool $isEnabled
* @param string $fieldName
* @param string $direction
*/
public function __construct($isEnabled = false, $fieldName = '', $direction = self::SORT_ASC)
{
$this->isEnabled = $isEnabled;
$this->setFieldName($fieldName);
$this->setDirection($direction);
}
/**
* @return Sorting
*/
public static function getEmpty()
{
return new Sorting(false);
}
/**
* @return string
*/
public function getFieldName(): string
{
return $this->fieldName;
}
/**
* @param string $fieldName
*/
public function setFieldName(string $fieldName)
{
$this->fieldName = $fieldName;
}
/**
* @return string
*/
public function getDirection(): string
{
return $this->direction;
}
/**
* @param string $direction
*/
public function setDirection(string $direction)
{
$this->direction = $direction;
}
/**
* Parses a sorting representation "<fieldName> <direction>"
* @param string $sortingString
* @return Sorting
*/
public static function fromString($sortingString)
{
$parts = GeneralUtility::trimExplode(' ', $sortingString);
return new Sorting(true, $parts[0], $parts[1]);
}
}

View File

@@ -0,0 +1,99 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\Query\ParameterBuilder;
/***************************************************************
* Copyright notice
*
* (c) 2017 <timo.hund@dkd.de>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project is
* free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* The Sorting ParameterProvider is responsible to build the solr query parameters
* that are needed for the sorting.
*/
class Sortings extends AbstractDeactivatable
{
/**
* @var array
*/
protected $sortings = [];
/**
* Sortings constructor.
* @param bool $isEnabled
* @param array $sortings
*/
public function __construct($isEnabled = false, $sortings = [])
{
$this->isEnabled = $isEnabled;
$this->setSortings($sortings);
}
/**
* @return Sortings
*/
public static function getEmpty()
{
return new Sortings(false);
}
/**
* @return Sorting[]
*/
public function getSortings(): array
{
return $this->sortings;
}
/**
* @param array $sortings
*/
public function setSortings(array $sortings)
{
$this->sortings = $sortings;
}
/**
* @param Sorting $sorting
*/
public function addSorting(Sorting $sorting)
{
$this->sortings[] = $sorting;
}
/**
* Parses a sortings representation "<fieldName> <direction>,<fieldName> <direction>"
* @param string $sortingsString
* @return Sortings
*/
public static function fromString($sortingsString)
{
$sortFields = GeneralUtility::trimExplode(',',$sortingsString);
$sortings = [];
foreach($sortFields as $sortField) {
$sorting = Sorting::fromString($sortField);
$sortings[] = $sorting;
}
return new Sortings(true, $sortings);
}
}

View File

@@ -0,0 +1,102 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\Query\ParameterBuilder;
/***************************************************************
* Copyright notice
*
* (c) 2017 <timo.hund@dkd.de>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project is
* free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use WapplerSystems\Meilisearch\Domain\Search\Query\AbstractQueryBuilder;
use WapplerSystems\Meilisearch\System\Configuration\TypoScriptConfiguration;
/**
* The Spellchecking ParameterProvider is responsible to build the solr query parameters
* that are needed for the spellchecking.
*/
class Spellchecking extends AbstractDeactivatable implements ParameterBuilder
{
/**
* @var int
*/
protected $maxCollationTries = 0;
/**
* Spellchecking constructor.
*
* @param bool $isEnabled
* @param int $maxCollationTries
*/
public function __construct($isEnabled = false, int $maxCollationTries = 0)
{
$this->isEnabled = $isEnabled;
$this->maxCollationTries = $maxCollationTries;
}
/**
* @return int
*/
public function getMaxCollationTries(): int
{
return $this->maxCollationTries;
}
/**
* @param TypoScriptConfiguration $solrConfiguration
* @return Spellchecking
*/
public static function fromTypoScriptConfiguration(TypoScriptConfiguration $solrConfiguration)
{
$isEnabled = $solrConfiguration->getSearchSpellchecking();
if (!$isEnabled) {
return new Spellchecking(false);
}
$maxCollationTries = $solrConfiguration->getSearchSpellcheckingNumberOfSuggestionsToTry();
return new Spellchecking($isEnabled, $maxCollationTries);
}
/**
* @return Spellchecking
*/
public static function getEmpty()
{
return new Spellchecking(false);
}
/**
* @param AbstractQueryBuilder $parentBuilder
* @return AbstractQueryBuilder
*/
public function build(AbstractQueryBuilder $parentBuilder): AbstractQueryBuilder
{
$query = $parentBuilder->getQuery();
if (!$this->getIsEnabled()) {
$query->removeComponent($query->getSpellcheck());
return $parentBuilder;
}
$query->getSpellcheck()->setMaxCollationTries($this->getMaxCollationTries());
$query->getSpellcheck()->setCollate(true);
return $parentBuilder;
}
}

View File

@@ -0,0 +1,94 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\Query\ParameterBuilder;
/***************************************************************
* Copyright notice
*
* (c) 2010-2017 dkd Internet Service GmbH <solr-support@dkd.de>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project is
* free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use WapplerSystems\Meilisearch\Domain\Search\Query\AbstractQueryBuilder;
use WapplerSystems\Meilisearch\System\Configuration\TypoScriptConfiguration;
/**
* The TrigramPhraseFields class
*/
class TrigramPhraseFields extends AbstractFieldList implements ParameterBuilder
{
/**
* Parameter key which should be used for Apache Solr URL query
*
* @var string
*/
protected $parameterKey = 'pf3';
/**
* Parses the string representation of the fieldList (e.g. content^100, title^10) to the object representation.
*
* @param string $fieldListString
* @param string $delimiter
* @return TrigramPhraseFields
*/
public static function fromString(string $fieldListString, string $delimiter = ',') : TrigramPhraseFields
{
return self::initializeFromString($fieldListString, $delimiter);
}
/**
* @param TypoScriptConfiguration $solrConfiguration
* @return TrigramPhraseFields
*/
public static function fromTypoScriptConfiguration(TypoScriptConfiguration $solrConfiguration)
{
$isEnabled = $solrConfiguration->getTrigramPhraseSearchIsEnabled();
if (!$isEnabled) {
return new TrigramPhraseFields(false);
}
return self::fromString((string)$solrConfiguration->getSearchQueryTrigramPhraseFields());
}
/**
* Parses the string representation of the fieldList (e.g. content^100, title^10) to the object representation.
*
* @param string $fieldListString
* @param string $delimiter
* @return TrigramPhraseFields
*/
protected static function initializeFromString(string $fieldListString, string $delimiter = ',') : TrigramPhraseFields
{
$fieldList = self::buildFieldList($fieldListString, $delimiter);
return new TrigramPhraseFields(true, $fieldList);
}
/**
* @param AbstractQueryBuilder $parentBuilder
* @return AbstractQueryBuilder
*/
public function build(AbstractQueryBuilder $parentBuilder): AbstractQueryBuilder
{
$trigramPhraseFieldsString = $this->toString();
if ($trigramPhraseFieldsString === '' || !$this->getIsEnabled()) {
return $parentBuilder;
}
$parentBuilder->getQuery()->getEDisMax()->setPhraseTrigramFields($trigramPhraseFieldsString);
return $parentBuilder;
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\Query;
/***************************************************************
* Copyright notice
*
* (c) 2009-2015 Ingo Renner <ingo@typo3.org>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project is
* free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use Solarium\QueryType\Select\Query\Query as SolariumQuery;
class Query extends SolariumQuery {
/**
* Returns the query parameters that should be used.
*
* @return array
*/
public function getQueryParameters() {
return $this->getParams();
}
/**
* @return string
*/
public function __toString()
{
return $this->getQuery();
}
}

View File

@@ -0,0 +1,587 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\Query;
/***************************************************************
* Copyright notice
*
* (c) 2017 <timo.hund@dkd.de>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project is
* free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use WapplerSystems\Meilisearch\Domain\Search\Query\ParameterBuilder\BigramPhraseFields;
use WapplerSystems\Meilisearch\Domain\Search\Query\ParameterBuilder\Elevation;
use WapplerSystems\Meilisearch\Domain\Search\Query\ParameterBuilder\Faceting;
use WapplerSystems\Meilisearch\Domain\Search\Query\ParameterBuilder\FieldCollapsing;
use WapplerSystems\Meilisearch\Domain\Search\Query\ParameterBuilder\Filters;
use WapplerSystems\Meilisearch\Domain\Search\Query\ParameterBuilder\Grouping;
use WapplerSystems\Meilisearch\Domain\Search\Query\ParameterBuilder\Highlighting;
use WapplerSystems\Meilisearch\Domain\Search\Query\ParameterBuilder\PhraseFields;
use WapplerSystems\Meilisearch\Domain\Search\Query\ParameterBuilder\QueryFields;
use WapplerSystems\Meilisearch\Domain\Search\Query\ParameterBuilder\ReturnFields;
use WapplerSystems\Meilisearch\Domain\Search\Query\ParameterBuilder\Slops;
use WapplerSystems\Meilisearch\Domain\Search\Query\ParameterBuilder\Sorting;
use WapplerSystems\Meilisearch\Domain\Search\Query\ParameterBuilder\Sortings;
use WapplerSystems\Meilisearch\Domain\Search\Query\ParameterBuilder\Spellchecking;
use WapplerSystems\Meilisearch\Domain\Search\Query\ParameterBuilder\TrigramPhraseFields;
use WapplerSystems\Meilisearch\Domain\Site\SiteHashService;
use WapplerSystems\Meilisearch\Domain\Site\SiteRepository;
use WapplerSystems\Meilisearch\FieldProcessor\PageUidToHierarchy;
use WapplerSystems\Meilisearch\System\Configuration\TypoScriptConfiguration;
use WapplerSystems\Meilisearch\System\Logging\SolrLogManager;
use WapplerSystems\Meilisearch\Util;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
/**
* The concrete QueryBuilder contains all TYPO3 specific initialization logic of solr queries, for TYPO3.
*/
class QueryBuilder extends AbstractQueryBuilder {
/**
* Additional filters, which will be added to the query, as well as to
* suggest queries.
*
* @var array
*/
protected $additionalFilters = [];
/**
* @var TypoScriptConfiguration
*/
protected $typoScriptConfiguration = null;
/**
* @var SolrLogManager;
*/
protected $logger = null;
/**
* @var SiteHashService
*/
protected $siteHashService = null;
/**
* QueryBuilder constructor.
* @param TypoScriptConfiguration|null $configuration
* @param SolrLogManager|null $solrLogManager
* @param SiteHashService|null $siteHashService
*/
public function __construct(TypoScriptConfiguration $configuration = null, SolrLogManager $solrLogManager = null, SiteHashService $siteHashService = null)
{
$this->typoScriptConfiguration = $configuration ?? Util::getSolrConfiguration();
$this->logger = $solrLogManager ?? GeneralUtility::makeInstance(SolrLogManager::class, /** @scrutinizer ignore-type */ __CLASS__);
$this->siteHashService = $siteHashService ?? GeneralUtility::makeInstance(SiteHashService::class);
}
/**
* @param string $queryString
* @return QueryBuilder
*/
public function newSearchQuery($queryString): QueryBuilder
{
$this->queryToBuild = $this->getSearchQueryInstance((string)$queryString);
return $this;
}
/**
* @param string $queryString
* @return QueryBuilder
*/
public function newSuggestQuery($queryString): QueryBuilder
{
$this->queryToBuild = $this->getSuggestQueryInstance($queryString);
return $this;
}
/**
* Initializes the Query object and SearchComponents and returns
* the initialized query object, when a search should be executed.
*
* @param string|null $rawQuery
* @param int $resultsPerPage
* @param array $additionalFiltersFromRequest
* @return SearchQuery
*/
public function buildSearchQuery($rawQuery, $resultsPerPage = 10, array $additionalFiltersFromRequest = []) : SearchQuery
{
if ($this->typoScriptConfiguration->getLoggingQuerySearchWords()) {
$this->logger->log(SolrLogManager::INFO, 'Received search query', [$rawQuery]);
}
/* @var $query SearchQuery */
return $this->newSearchQuery($rawQuery)
->useResultsPerPage($resultsPerPage)
->useReturnFieldsFromTypoScript()
->useQueryFieldsFromTypoScript()
->useInitialQueryFromTypoScript()
->useFiltersFromTypoScript()
->useFilterArray($additionalFiltersFromRequest)
->useFacetingFromTypoScript()
->useVariantsFromTypoScript()
->useGroupingFromTypoScript()
->useHighlightingFromTypoScript()
->usePhraseFieldsFromTypoScript()
->useBigramPhraseFieldsFromTypoScript()
->useTrigramPhraseFieldsFromTypoScript()
->useOmitHeader(false)
->getQuery();
}
/**
* Builds a SuggestQuery with all applied filters.
*
* @param string $queryString
* @param array $additionalFilters
* @param integer $requestedPageId
* @param string $groupList
* @return SuggestQuery
*/
public function buildSuggestQuery(string $queryString, array $additionalFilters, int $requestedPageId, string $groupList) : SuggestQuery
{
$this->newSuggestQuery($queryString)
->useFiltersFromTypoScript()
->useSiteHashFromTypoScript($requestedPageId)
->useUserAccessGroups(explode(',', $groupList))
->useOmitHeader();
if (!empty($additionalFilters)) {
$this->useFilterArray($additionalFilters);
}
return $this->queryToBuild;
}
/**
* Returns Query for Search which finds document for given page.
* Note: The Connection is per language as recommended in ext-solr docs.
*
* @return Query
*/
public function buildPageQuery($pageId)
{
$siteRepository = GeneralUtility::makeInstance(SiteRepository::class);
$site = $siteRepository->getSiteByPageId($pageId);
return $this->newSearchQuery('')
->useQueryString('*:*')
->useFilter('(type:pages AND uid:' . $pageId . ') OR (*:* AND pid:' . $pageId . ' NOT type:pages)', 'type')
->useFilter('siteHash:' . $site->getSiteHash(), 'siteHash')
->useReturnFields(ReturnFields::fromString('*'))
->useSortings(Sortings::fromString('type asc, title asc'))
->useQueryType('standard')
->getQuery();
}
/**
* Returns a query for single record
*
* @return Query
*/
public function buildRecordQuery($type, $uid, $pageId): Query
{
$siteRepository = GeneralUtility::makeInstance(SiteRepository::class);
$site = $siteRepository->getSiteByPageId($pageId);
return $this->newSearchQuery('')
->useQueryString('*:*')
->useFilter('type:' . $type . ' AND uid:' . $uid, 'type')
->useFilter('siteHash:' . $site->getSiteHash(), 'siteHash')
->useReturnFields(ReturnFields::fromString('*'))
->useSortings(Sortings::fromString('type asc, title asc'))
->useQueryType('standard')
->getQuery();
}
/**
* @return QueryBuilder
*/
public function useSlopsFromTypoScript(): QueryBuilder
{
return $this->useSlops(Slops::fromTypoScriptConfiguration($this->typoScriptConfiguration));
}
/**
* Uses the configured boost queries from typoscript
*
* @return QueryBuilder
*/
public function useBoostQueriesFromTypoScript(): QueryBuilder
{
$searchConfiguration = $this->typoScriptConfiguration->getSearchConfiguration();
if (!empty($searchConfiguration['query.']['boostQuery'])) {
return $this->useBoostQueries($searchConfiguration['query.']['boostQuery']);
}
if (!empty($searchConfiguration['query.']['boostQuery.'])) {
$boostQueries = $searchConfiguration['query.']['boostQuery.'];
return $this->useBoostQueries(array_values($boostQueries));
}
return $this;
}
/**
* Uses the configured boostFunction from the typoscript configuration.
*
* @return QueryBuilder
*/
public function useBoostFunctionFromTypoScript(): QueryBuilder
{
$searchConfiguration = $this->typoScriptConfiguration->getSearchConfiguration();
if (!empty($searchConfiguration['query.']['boostFunction'])) {
return $this->useBoostFunction($searchConfiguration['query.']['boostFunction']);
}
return $this;
}
/**
* Uses the configured minimumMatch from the typoscript configuration.
*
* @return QueryBuilder
*/
public function useMinimumMatchFromTypoScript(): QueryBuilder
{
$searchConfiguration = $this->typoScriptConfiguration->getSearchConfiguration();
if (!empty($searchConfiguration['query.']['minimumMatch'])) {
return $this->useMinimumMatch($searchConfiguration['query.']['minimumMatch']);
}
return $this;
}
/**
* @return QueryBuilder
*/
public function useTieParameterFromTypoScript(): QueryBuilder
{
$searchConfiguration = $this->typoScriptConfiguration->getSearchConfiguration();
if (empty($searchConfiguration['query.']['tieParameter'])) {
return $this;
}
return $this->useTieParameter($searchConfiguration['query.']['tieParameter']);
}
/**
* Applies the configured query fields from the typoscript configuration.
*
* @return QueryBuilder
*/
public function useQueryFieldsFromTypoScript(): QueryBuilder
{
return $this->useQueryFields(QueryFields::fromString($this->typoScriptConfiguration->getSearchQueryQueryFields()));
}
/**
* Applies the configured return fields from the typoscript configuration.
*
* @return QueryBuilder
*/
public function useReturnFieldsFromTypoScript(): QueryBuilder
{
$returnFieldsArray = (array)$this->typoScriptConfiguration->getSearchQueryReturnFieldsAsArray(['*', 'score']);
return $this->useReturnFields(ReturnFields::fromArray($returnFieldsArray));
}
/**
* Can be used to apply the allowed sites from plugin.tx_meilisearch.search.query.allowedSites to the query.
*
* @param int $requestedPageId
* @return QueryBuilder
*/
public function useSiteHashFromTypoScript(int $requestedPageId): QueryBuilder
{
$queryConfiguration = $this->typoScriptConfiguration->getObjectByPathOrDefault('plugin.tx_meilisearch.search.query.', []);
$allowedSites = $this->siteHashService->getAllowedSitesForPageIdAndAllowedSitesConfiguration($requestedPageId, $queryConfiguration['allowedSites']);
return $this->useSiteHashFromAllowedSites($allowedSites);
}
/**
* Can be used to apply a list of allowed sites to the query.
*
* @param string $allowedSites
* @return QueryBuilder
*/
public function useSiteHashFromAllowedSites($allowedSites): QueryBuilder
{
$isAnySiteAllowed = trim($allowedSites) === '*';
if ($isAnySiteAllowed) {
// no filter required
return $this;
}
$allowedSites = GeneralUtility::trimExplode(',', $allowedSites);
$filters = [];
foreach ($allowedSites as $site) {
$siteHash = $this->siteHashService->getSiteHashForDomain($site);
$filters[] = 'siteHash:"' . $siteHash . '"';
}
$siteHashFilterString = implode(' OR ', $filters);
return $this->useFilter($siteHashFilterString, 'siteHash');
}
/**
* Can be used to filter the result on an applied list of user groups.
*
* @param array $groups
* @return QueryBuilder
*/
public function useUserAccessGroups(array $groups): QueryBuilder
{
$groups = array_map('intval', $groups);
$groups[] = 0; // always grant access to public documents
$groups = array_unique($groups);
sort($groups, SORT_NUMERIC);
$accessFilter = '{!typo3access}' . implode(',', $groups);
$this->queryToBuild->removeFilterQuery('access');
return $this->useFilter($accessFilter, 'access');
}
/**
* Applies the configured initial query settings to set the alternative query for solr as required.
*
* @return QueryBuilder
*/
public function useInitialQueryFromTypoScript(): QueryBuilder
{
if ($this->typoScriptConfiguration->getSearchInitializeWithEmptyQuery() || $this->typoScriptConfiguration->getSearchQueryAllowEmptyQuery()) {
// empty main query, but using a "return everything"
// alternative query in q.alt
$this->useAlternativeQuery('*:*');
}
if ($this->typoScriptConfiguration->getSearchInitializeWithQuery()) {
$this->useAlternativeQuery($this->typoScriptConfiguration->getSearchInitializeWithQuery());
}
return $this;
}
/**
* Applies the configured facets from the typoscript configuration on the query.
*
* @return QueryBuilder
*/
public function useFacetingFromTypoScript(): QueryBuilder
{
return $this->useFaceting(Faceting::fromTypoScriptConfiguration($this->typoScriptConfiguration));
}
/**
* Applies the configured variants from the typoscript configuration on the query.
*
* @return QueryBuilder
*/
public function useVariantsFromTypoScript(): QueryBuilder
{
return $this->useFieldCollapsing(FieldCollapsing::fromTypoScriptConfiguration($this->typoScriptConfiguration));
}
/**
* Applies the configured groupings from the typoscript configuration to the query.
*
* @return QueryBuilder
*/
public function useGroupingFromTypoScript(): QueryBuilder
{
return $this->useGrouping(Grouping::fromTypoScriptConfiguration($this->typoScriptConfiguration));
}
/**
* Applies the configured highlighting from the typoscript configuration to the query.
*
* @return QueryBuilder
*/
public function useHighlightingFromTypoScript(): QueryBuilder
{
return $this->useHighlighting(Highlighting::fromTypoScriptConfiguration($this->typoScriptConfiguration));
}
/**
* Applies the configured filters (page section and other from typoscript).
*
* @return QueryBuilder
*/
public function useFiltersFromTypoScript(): QueryBuilder
{
$filters = Filters::fromTypoScriptConfiguration($this->typoScriptConfiguration);
$this->queryToBuild->setFilterQueries($filters->getValues());
$this->useFilterArray($this->getAdditionalFilters());
$searchQueryFilters = $this->typoScriptConfiguration->getSearchQueryFilterConfiguration();
if (!is_array($searchQueryFilters) || count($searchQueryFilters) <= 0) {
return $this;
}
// special filter to limit search to specific page tree branches
if (array_key_exists('__pageSections', $searchQueryFilters)) {
$pageIds = GeneralUtility::trimExplode(',', $searchQueryFilters['__pageSections']);
$this->usePageSectionsFromPageIds($pageIds);
$this->typoScriptConfiguration->removeSearchQueryFilterForPageSections();
}
return $this;
}
/**
* Applies the configured elevation from the typoscript configuration.
*
* @return QueryBuilder
*/
public function useElevationFromTypoScript(): QueryBuilder
{
return $this->useElevation(Elevation::fromTypoScriptConfiguration($this->typoScriptConfiguration));
}
/**
* Applies the configured spellchecking from the typoscript configuration.
*
* @return QueryBuilder
*/
public function useSpellcheckingFromTypoScript(): QueryBuilder
{
return $this->useSpellchecking(Spellchecking::fromTypoScriptConfiguration($this->typoScriptConfiguration));
}
/**
* Applies the passed pageIds as __pageSection filter.
*
* @param array $pageIds
* @return QueryBuilder
*/
public function usePageSectionsFromPageIds(array $pageIds = []): QueryBuilder
{
$filters = [];
/** @var $processor PageUidToHierarchy */
$processor = GeneralUtility::makeInstance(PageUidToHierarchy::class);
$hierarchies = $processor->process($pageIds);
foreach ($hierarchies as $hierarchy) {
$lastLevel = array_pop($hierarchy);
$filters[] = 'rootline:"' . $lastLevel . '"';
}
$pageSectionsFilterString = implode(' OR ', $filters);
return $this->useFilter($pageSectionsFilterString, 'pageSections');
}
/**
* Applies the configured phrase fields from the typoscript configuration to the query.
*
* @return QueryBuilder
*/
public function usePhraseFieldsFromTypoScript(): QueryBuilder
{
return $this->usePhraseFields(PhraseFields::fromTypoScriptConfiguration($this->typoScriptConfiguration));
}
/**
* Applies the configured bigram phrase fields from the typoscript configuration to the query.
*
* @return QueryBuilder
*/
public function useBigramPhraseFieldsFromTypoScript(): QueryBuilder
{
return $this->useBigramPhraseFields(BigramPhraseFields::fromTypoScriptConfiguration($this->typoScriptConfiguration));
}
/**
* Applies the configured trigram phrase fields from the typoscript configuration to the query.
*
* @return QueryBuilder
*/
public function useTrigramPhraseFieldsFromTypoScript(): QueryBuilder
{
return $this->useTrigramPhraseFields(TrigramPhraseFields::fromTypoScriptConfiguration($this->typoScriptConfiguration));
}
/**
* Retrieves the configuration filters from the TypoScript configuration, except the __pageSections filter.
*
* @return array
*/
public function getAdditionalFilters() : array
{
// when we've build the additionalFilter once, we could return them
if (count($this->additionalFilters) > 0) {
return $this->additionalFilters;
}
$searchQueryFilters = $this->typoScriptConfiguration->getSearchQueryFilterConfiguration();
if (!is_array($searchQueryFilters) || count($searchQueryFilters) <= 0) {
return [];
}
$cObj = GeneralUtility::makeInstance(ContentObjectRenderer::class);
// all other regular filters
foreach ($searchQueryFilters as $filterKey => $filter) {
// the __pageSections filter should not be handled as additional filter
if ($filterKey === '__pageSections') {
continue;
}
$filterIsArray = is_array($searchQueryFilters[$filterKey]);
if ($filterIsArray) {
continue;
}
$hasSubConfiguration = is_array($searchQueryFilters[$filterKey . '.']);
if ($hasSubConfiguration) {
$filter = $cObj->stdWrap($searchQueryFilters[$filterKey], $searchQueryFilters[$filterKey . '.']);
}
$this->additionalFilters[$filterKey] = $filter;
}
return $this->additionalFilters;
}
/**
* @param string $rawQuery
* @return SearchQuery
*/
protected function getSearchQueryInstance(string $rawQuery): SearchQuery
{
$query = GeneralUtility::makeInstance(SearchQuery::class);
$query->setQuery($rawQuery);
return $query;
}
/**
* @param string $rawQuery
* @return SuggestQuery
*/
protected function getSuggestQueryInstance($rawQuery): SuggestQuery
{
$query = GeneralUtility::makeInstance(SuggestQuery::class, /** @scrutinizer ignore-type */ $rawQuery, /** @scrutinizer ignore-type */ $this->typoScriptConfiguration);
return $query;
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\Query;
/***************************************************************
* Copyright notice
*
* (c) 2009-2015 Ingo Renner <ingo@typo3.org>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project is
* free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
class SearchQuery extends Query {}

View File

@@ -0,0 +1,86 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\Query;
/***************************************************************
* Copyright notice
*
* (c) 2009-2015 Ingo Renner <ingo@typo3.org>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project is
* free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use WapplerSystems\Meilisearch\Domain\Search\Query\ParameterBuilder\ReturnFields;
use WapplerSystems\Meilisearch\Domain\Search\Query\Helper\EscapeService;
use WapplerSystems\Meilisearch\System\Configuration\TypoScriptConfiguration;
use WapplerSystems\Meilisearch\Util;
/**
* A query specialized to get search suggestions
*
* @author Ingo Renner <ingo@typo3.org>
*/
class SuggestQuery extends Query
{
/**
* @var array
*/
protected $configuration;
/**
* @var string
*/
protected $prefix;
/**
* SuggestQuery constructor.
*
* @param string $keywords
* @param TypoScriptConfiguration $solrConfiguration
*/
public function __construct($keywords, $solrConfiguration = null)
{
parent::__construct();
$keywords = (string)$keywords;
$solrConfiguration = $solrConfiguration ?? Util::getSolrConfiguration();
$this->setQuery($keywords);
$this->configuration = $solrConfiguration->getObjectByPathOrDefault('plugin.tx_meilisearch.suggest.', []);
if (!empty($this->configuration['treatMultipleTermsAsSingleTerm'])) {
$this->prefix = EscapeService::escape($keywords);
} else {
$matches = [];
preg_match('/^(:?(.* |))([^ ]+)$/', $keywords, $matches);
$fullKeywords = trim($matches[2]);
$partialKeyword = trim($matches[3]);
$this->setQuery($fullKeywords);
$this->prefix = EscapeService::escape($partialKeyword);
}
$this->getEDisMax()->setQueryAlternative('*:*');
$this->setFields(ReturnFields::fromString($this->configuration['suggestField'])->getValues());
$this->addParam('facet', 'on');
$this->addParam('facet.prefix', $this->prefix);
$this->addParam('facet.field', $this->configuration['suggestField']);
$this->addParam('facet.limit', $this->configuration['numberOfSuggestions']);
$this->addParam('facet.mincount', 1);
$this->addParam('facet.method', 'enum');
}
}

View File

@@ -0,0 +1,290 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\SearchResultSet;
/**
* Value object that represent a options facet.
*
* @author Frans Saris <frans@beech.it>
* @author Timo Hund <timo.hund@dkd.de>
*/
abstract class AbstractFacet
{
const TYPE_ABSTRACT = 'abstract';
/**
* String
* @var string
*/
protected static $type = self::TYPE_ABSTRACT;
/**
* The resultSet where this facet belongs to.
*
* @var SearchResultSet
*/
protected $resultSet = null;
/**
* @var string
*/
protected $name;
/**
* @var string
*/
protected $field;
/**
* @var string
*/
protected $label;
/**
* @var array
*/
protected $configuration;
/**
* @var boolean
*/
protected $isAvailable = false;
/**
* @var bool
*/
protected $isUsed = false;
/**
* @var bool
*/
protected $allRequirementsMet = true;
/**
* @var \TYPO3\CMS\Extbase\Object\ObjectManagerInterface
*/
protected $objectManager;
/**
* AbstractFacet constructor.
*
* @param SearchResultSet $resultSet
* @param string $name
* @param string $field
* @param string $label
* @param array $configuration Facet configuration passed from typoscript
*/
public function __construct(SearchResultSet $resultSet, $name, $field, $label = '', array $configuration = [])
{
$this->resultSet = $resultSet;
$this->name = $name;
$this->field = $field;
$this->label = $label;
$this->configuration = $configuration;
}
/**
* Injects the object manager
*
* @param \TYPO3\CMS\Extbase\Object\ObjectManagerInterface $objectManager
*/
public function injectObjectManager(\TYPO3\CMS\Extbase\Object\ObjectManagerInterface $objectManager)
{
$this->objectManager = $objectManager;
}
/**
* Get name
*
* @return string
*/
public function getName()
{
return $this->name;
}
/**
* Get solr field name
*
* @return string
*/
public function getField()
{
return $this->field;
}
/**
* @param string $label
*/
public function setLabel($label)
{
$this->label = $label;
}
/**
* @return string
*/
public function getLabel()
{
return $this->label;
}
/**
* @param boolean $isAvailable
*/
public function setIsAvailable($isAvailable)
{
$this->isAvailable = $isAvailable;
}
/**
* @return boolean
*/
public function getIsAvailable()
{
return $this->isAvailable;
}
/**
* @param boolean $isUsed
*/
public function setIsUsed($isUsed)
{
$this->isUsed = $isUsed;
}
/**
* @return boolean
*/
public function getIsUsed()
{
return $this->isUsed;
}
/**
* @return string
*/
public function getType()
{
return static::$type;
}
/**
* @return boolean
*/
public function getAllRequirementsMet()
{
return $this->allRequirementsMet;
}
/**
* @param boolean $allRequirementsMet
*/
public function setAllRequirementsMet($allRequirementsMet)
{
$this->allRequirementsMet = $allRequirementsMet;
}
/**
* @return SearchResultSet
*/
public function getResultSet()
{
return $this->resultSet;
}
/**
* Get configuration
*
* @return mixed
*/
public function getConfiguration()
{
return $this->configuration;
}
/**
* Get facet partial name used for rendering the facet
*
* @return string
*/
public function getPartialName()
{
return 'Default';
}
/**
* @return string
*/
public function getGroupName()
{
return isset($this->configuration['groupName']) ? $this->configuration['groupName'] : 'main';
}
/**
* Indicates if this facet should ne included in the available facets. When nothing is configured,
* the method return TRUE.
*
* @return boolean
*/
public function getIncludeInAvailableFacets()
{
return ((int)$this->getFacetSettingOrDefaultValue('includeInAvailableFacets', 1)) === 1;
}
/**
* Indicates if this facets should be included in the used facets. When nothing is configured,
* the methods returns true.
*
* @return boolean
*/
public function getIncludeInUsedFacets()
{
return ((int)$this->getFacetSettingOrDefaultValue('includeInUsedFacets', 1)) === 1;
}
/**
* Returns the configured requirements
*
* @return array
*/
public function getRequirements()
{
return $this->getFacetSettingOrDefaultValue('requirements.', []);
}
/**
* The implementation of this method should return a "flatten" collection of all items.
*
* @return AbstractFacetItemCollection
*/
abstract public function getAllFacetItems();
/**
* @param string $key
* @param mixed $defaultValue
* @return mixed
*/
protected function getFacetSettingOrDefaultValue($key, $defaultValue)
{
if (!isset($this->configuration[$key])) {
return $defaultValue;
}
return ($this->configuration[$key]);
}
}

View File

@@ -0,0 +1,116 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\SearchResultSet;
/**
* Abstract item that represent a value of a facet. E.g. an option or a node
*
* @author Frans Saris <frans@beech.it>
* @author Timo Hund <timo.hund@dkd.de>
*/
abstract class AbstractFacetItem
{
/**
* @var string
*/
protected $label = '';
/**
* @var int
*/
protected $documentCount = 0;
/**
* @var bool
*/
protected $selected = false;
/**
* @var array
*/
protected $metrics = [];
/**
* @var AbstractFacet
*/
protected $facet;
/**
* @param AbstractFacet $facet
* @param string $label
* @param int $documentCount
* @param bool $selected
* @param array $metrics
*/
public function __construct(AbstractFacet $facet, $label = '', $documentCount = 0, $selected = false, $metrics = [])
{
$this->facet = $facet;
$this->label = $label;
$this->documentCount = $documentCount;
$this->selected = $selected;
$this->metrics = $metrics;
}
/**
* @return int
*/
public function getDocumentCount()
{
return $this->documentCount;
}
/**
* @return \WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\AbstractFacet
*/
public function getFacet()
{
return $this->facet;
}
/**
* @return string
*/
public function getLabel()
{
return $this->label;
}
/**
* @return boolean
*/
public function getSelected()
{
return $this->selected;
}
/**
* @return array
*/
public function getMetrics()
{
return $this->metrics;
}
/**
* @return string
*/
abstract public function getUriValue();
/**
* @return string
*/
abstract function getCollectionKey();
}

View File

@@ -0,0 +1,102 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\AbstractFacetItem;
use WapplerSystems\Meilisearch\System\Data\AbstractCollection;
/**
* Collection for facet options.
*
* @author Frans Saris <frans@beech.it>
* @author Timo Hund <timo.hund@dkd.de>
*/
abstract class AbstractFacetItemCollection extends AbstractCollection
{
/**
* @param AbstractFacetItem $item
* @return AbstractFacetItemCollection
*/
public function add($item)
{
if ($item === null) {
return $this;
}
$this->data[$item->getCollectionKey()] = $item;
return $this;
}
/**
* @param string $value
* @return AbstractFacetItem
*/
public function getByValue($value)
{
return isset($this->data[$value]) ? $this->data[$value] : null;
}
/**
* Retrieves the count (with get prefixed to be usable in fluid).
*
* @return int
*/
public function getCount()
{
return $this->count();
}
/**
* @return AbstractFacetItemCollection
*/
public function getSelected()
{
return $this->getFilteredCopy(function(AbstractFacetItem $item) {
return $item->getSelected();
});
}
/**
* @param array $manualSorting
* @return AbstractFacetItemCollection
*/
public function getManualSortedCopy(array $manualSorting)
{
$result = clone $this;
$copiedItems = $result->data;
$sortedOptions = [];
foreach ($manualSorting as $item) {
if (isset($copiedItems[$item])) {
$sortedOptions[$item] = $copiedItems[$item];
unset($copiedItems[$item]);
}
}
// in the end all items get appended that are not configured in the manual sort order
$sortedOptions = $sortedOptions + $copiedItems;
$result->data = $sortedOptions;
return $result;
}
/**
* @return AbstractFacetItemCollection
*/
public function getReversedOrderCopy()
{
$result = clone $this;
$result->data = array_reverse($result->data, true);
return $result;
}
}

View File

@@ -0,0 +1,92 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Extbase\Object\ObjectManagerInterface;
/**
* Class AbstractFacetPackage
*/
abstract class AbstractFacetPackage {
/**
* @var ObjectManagerInterface
*/
protected $objectManager;
/**
* @param ObjectManagerInterface $objectManager
*/
public function setObjectManager(ObjectManagerInterface $objectManager)
{
$this->objectManager = $objectManager;
}
/**
* @return string
*/
abstract public function getParserClassName();
/**
* @return FacetParserInterface
* @throws InvalidFacetPackageException
*/
public function getParser()
{
$parser = $this->objectManager->get($this->getParserClassName());
if (!$parser instanceof FacetParserInterface) {
throw new InvalidFacetPackageException('Invalid parser for package ' . __CLASS__);
}
return $parser;
}
/**
* @return string
*/
public function getUrlDecoderClassName() {
return (string)DefaultUrlDecoder::class;
}
/**
* @throws InvalidUrlDecoderException
* @return FacetUrlDecoderInterface
*/
public function getUrlDecoder()
{
$urlDecoder = $this->objectManager->get($this->getUrlDecoderClassName());
if (!$urlDecoder instanceof FacetUrlDecoderInterface) {
throw new InvalidUrlDecoderException('Invalid urldecoder for package ' . __CLASS__);
}
return $urlDecoder;
}
/**
* @return string
*/
public function getQueryBuilderClassName() {
return (string)DefaultFacetQueryBuilder::class;
}
/**
* @throws InvalidQueryBuilderException
* @return FacetQueryBuilderInterface
*/
public function getQueryBuilder()
{
$urlDecoder = $this->objectManager->get($this->getQueryBuilderClassName());
if(!$urlDecoder instanceof FacetQueryBuilderInterface) {
throw new InvalidQueryBuilderException('Invalid querybuilder for package ' . __CLASS__);
}
return $urlDecoder;
}
}

View File

@@ -0,0 +1,189 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\OptionBased\AbstractOptionsFacet;
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\SearchResultSet;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Object\ObjectManagerInterface;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
/**
* Class AbstractFacetParser
*
* @author Frans Saris <frans@beech.it>
* @author Timo Hund <timo.hund@dkd.de>
*/
abstract class AbstractFacetParser implements FacetParserInterface
{
/**
* @var ContentObjectRenderer
*/
protected static $reUseAbleContentObject;
/**
* @var ObjectManagerInterface
*/
protected $objectManager;
/**
* @param ObjectManagerInterface $objectManager
*/
public function injectObjectManager(ObjectManagerInterface $objectManager)
{
$this->objectManager = $objectManager;
}
/**
* @return ContentObjectRenderer
*/
protected function getReUseAbleContentObject()
{
/** @var $contentObject ContentObjectRenderer */
if (self::$reUseAbleContentObject !== null) {
return self::$reUseAbleContentObject;
}
self::$reUseAbleContentObject = GeneralUtility::makeInstance(ContentObjectRenderer::class);
return self::$reUseAbleContentObject;
}
/**
* @param array $configuration
* @return string
*/
protected function getPlainLabelOrApplyCObject($configuration)
{
// when no label is configured we return an empty string
if (!isset($configuration['label'])) {
return '';
}
// when no sub configuration is set, we use the string, configured as label
if (!isset($configuration['label.'])) {
return $configuration['label'];
}
// when label and label. was set, we apply the cObject
return $this->getReUseAbleContentObject()->cObjGetSingle($configuration['label'], $configuration['label.']);
}
/**
* @param mixed $value
* @param integer $count
* @param string $facetName
* @param array $facetConfiguration
* @return string
*/
protected function getLabelFromRenderingInstructions($value, $count, $facetName, $facetConfiguration)
{
$hasRenderingInstructions = isset($facetConfiguration['renderingInstruction']) && isset($facetConfiguration['renderingInstruction.']);
if (!$hasRenderingInstructions) {
return $value;
}
$this->getReUseAbleContentObject()->start(['optionValue' => $value, 'optionCount' => $count, 'facetName' => $facetName]);
return $this->getReUseAbleContentObject()->cObjGetSingle(
$facetConfiguration['renderingInstruction'],
$facetConfiguration['renderingInstruction.']
);
}
/**
* Retrieves the active facetValue for a facet from the search request.
* @param SearchResultSet $resultSet
* @param string $facetName
* @return array
*/
protected function getActiveFacetValuesFromRequest(SearchResultSet $resultSet, $facetName)
{
$activeFacetValues = $resultSet->getUsedSearchRequest()->getActiveFacetValuesByName($facetName);
$activeFacetValues = is_array($activeFacetValues) ? $activeFacetValues : [];
return $activeFacetValues;
}
/**
* @param array $facetValuesFromSolrResponse
* @param array $facetValuesFromSearchRequest
* @return mixed
*/
protected function getMergedFacetValueFromSearchRequestAndSolrResponse($facetValuesFromSolrResponse, $facetValuesFromSearchRequest)
{
$facetValueItemsToCreate = $facetValuesFromSolrResponse;
foreach ($facetValuesFromSearchRequest as $valueFromRequest) {
// if we have options in the request that have not been in the response we add them with a count of 0
if (!isset($facetValueItemsToCreate[$valueFromRequest])) {
$facetValueItemsToCreate[$valueFromRequest] = 0;
}
}
return $facetValueItemsToCreate;
}
/**
* @param AbstractOptionsFacet $facet
* @param array $facetConfiguration
* @return AbstractOptionsFacet
*/
protected function applyManualSortOrder(AbstractOptionsFacet $facet, array $facetConfiguration)
{
if (!isset($facetConfiguration['manualSortOrder'])) {
return $facet;
}
$fields = GeneralUtility::trimExplode(',', $facetConfiguration['manualSortOrder']);
// @extensionScannerIgnoreLine
$sortedOptions = $facet->getOptions()->getManualSortedCopy($fields);
// @extensionScannerIgnoreLine
$facet->setOptions($sortedOptions);
return $facet;
}
/**
* @param AbstractOptionsFacet $facet
* @param array $facetConfiguration
* @return AbstractOptionsFacet
*/
protected function applyReverseOrder(AbstractOptionsFacet $facet, array $facetConfiguration)
{
if (empty($facetConfiguration['reverseOrder'])) {
return $facet;
}
// @extensionScannerIgnoreLine
$facet->setOptions($facet->getOptions()->getReversedOrderCopy());
return $facet;
}
/**
* @param mixed $value
* @param array $facetConfiguration
* @return boolean
*/
protected function getIsExcludedFacetValue($value, array $facetConfiguration)
{
if (!isset($facetConfiguration['excludeValues'])) {
return false;
}
$excludedValue = GeneralUtility::trimExplode(',', $facetConfiguration['excludeValues']);
return in_array($value, $excludedValue);
}
}

View File

@@ -0,0 +1,65 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use WapplerSystems\Meilisearch\System\Configuration\TypoScriptConfiguration;
class DefaultFacetQueryBuilder implements FacetQueryBuilderInterface {
/**
* @param string $facetName
* @param TypoScriptConfiguration $configuration
* @return array
*/
public function build($facetName, TypoScriptConfiguration $configuration)
{
$facetParameters = [];
$facetConfiguration = $configuration->getSearchFacetingFacetByName($facetName);
$tags = $this->buildExcludeTags($facetConfiguration, $configuration);
$facetParameters['facet.field'][] = $tags . $facetConfiguration['field'];
$sortingExpression = new SortingExpression();
$facetSortExpression = $sortingExpression->getForFacet($facetConfiguration['sortBy']);
if (!empty($facetSortExpression)) {
$facetParameters['f.' . $facetConfiguration['field'] . '.facet.sort'] = $facetSortExpression;
}
return $facetParameters;
}
/**
* @param array $facetConfiguration
* @param TypoScriptConfiguration $configuration
* @return string
*/
protected function buildExcludeTags(array $facetConfiguration, TypoScriptConfiguration $configuration)
{
// simple for now, may add overrides f.<field_name>.facet.* later
if ($configuration->getSearchFacetingKeepAllFacetsOnSelection()) {
$facets = [];
foreach ($configuration->getSearchFacetingFacets() as $facet) {
$facets[] = $facet['field'];
}
return '{!ex=' . implode(',', $facets) . '}';
} elseif ($facetConfiguration['keepAllOptionsOnSelection'] == 1) {
return '{!ex=' . $facetConfiguration['field'] . '}';
}
return '';
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
class DefaultUrlDecoder implements FacetUrlDecoderInterface {
/**
* Parses the query filter from GET parameters in the URL and translates it
* to a Lucene filter value.
*
* @param string $value the filter query from plugin
* @param array $configuration Facet configuration
* @return string Value to be used in a Lucene filter
*/
public function decode($value, array $configuration = [])
{
return '"' . addslashes($value) . '"';
}
}

View File

@@ -0,0 +1,78 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets;
use WapplerSystems\Meilisearch\System\Data\AbstractCollection;
/**
* Class FacetCollection
*/
class FacetCollection extends AbstractCollection
{
/**
* @param AbstractFacet $facet
*/
public function addFacet(AbstractFacet $facet)
{
$this->data[$facet->getName()] = $facet;
}
/**
* @return FacetCollection
*/
public function getUsed()
{
return $this->getFilteredCopy(
function(AbstractFacet $facet) {
return $facet->getIsUsed() && $facet->getIncludeInUsedFacets();
}
);
}
/**
* @return FacetCollection
*/
public function getAvailable()
{
return $this->getFilteredCopy(
function(AbstractFacet $facet) {
return $facet->getIsAvailable() && $facet->getIncludeInAvailableFacets() && $facet->getAllRequirementsMet();
}
);
}
/**
* @param string $requiredGroup
* @return AbstractCollection
*/
public function getByGroupName($requiredGroup = 'all')
{
return $this->getFilteredCopy(
function(AbstractFacet $facet) use ($requiredGroup) {
return $facet->getGroupName() == $requiredGroup;
}
);
}
/**
* @param string $requiredName
* @return AbstractCollection
*/
public function getByName($requiredName) {
return $this->getFilteredCopy(
function(AbstractFacet $facet) use ($requiredName) {
return $facet->getName() == $requiredName;
}
);
}
/**
* @param int $position
* @return AbstractFacet
*/
public function getByPosition($position)
{
return parent::getByPosition($position);
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\SearchResultSet;
/**
* Interface FacetParserInterface
*
* @author Frans Saris <frans@beech.it>
* @author Timo Hund <timo.hund@dkd.de>
*/
interface FacetParserInterface
{
/**
* @param SearchResultSet $resultSet
* @param string $facetName
* @param array $facetConfiguration
* @return AbstractFacet|null
*/
public function parse(SearchResultSet $resultSet, $facetName, array $facetConfiguration);
}

View File

@@ -0,0 +1,41 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets;
/***************************************************************
* Copyright notice
*
* (c) 2012-2015 Ingo Renner <ingo@typo3.org>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project is
* free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use WapplerSystems\Meilisearch\System\Configuration\TypoScriptConfiguration;
/**
* Query Filter Encoder Interface
*
* @author Ingo Renner <ingo@typo3.org>
*/
interface FacetQueryBuilderInterface
{
/**
* @param string $facetName
* @param TypoScriptConfiguration $configuration
* @return array
*/
public function build($facetName, TypoScriptConfiguration $configuration);
}

View File

@@ -0,0 +1,108 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\OptionBased\Hierarchy\HierarchyPackage;
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\OptionBased\Options\OptionsPackage;
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\OptionBased\QueryGroup\QueryGroupPackage;
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\RangeBased\DateRange\DateRangePackage;
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\RangeBased\NumericRange\NumericRangePackage;
use WapplerSystems\Meilisearch\System\Object\AbstractClassRegistry;
/**
* Class FacetRegistry
*
* @author Frans Saris <frans@beech.it>
* @author Timo Hund <timo.hund@dkd.de>
*/
class FacetRegistry extends AbstractClassRegistry
{
/**
* Array of available parser classNames
*
* @var array
*/
protected $classMap = [
'options' => OptionsPackage::class,
'hierarchy' => HierarchyPackage::class,
'queryGroup' => QueryGroupPackage::class,
'dateRange' => DateRangePackage::class,
'numericRange' => NumericRangePackage::class,
];
/**
* Default parser className
*
* @var string
*/
protected $defaultClass = OptionsPackage::class;
/**
* Get defaultParser
*
* @return string
*/
public function getDefaultPackage()
{
return $this->defaultClass;
}
/**
* Set defaultParser
*
* @param string $defaultPackageClassName
*/
public function setDefaultPackage($defaultPackageClassName)
{
$this->defaultClass = $defaultPackageClassName;
}
/**
* Get registered parser classNames
*
* @return array
*/
public function getPackages()
{
return $this->classMap;
}
/**
* @param string $className
* @param string $type
* @throws \InvalidArgumentException
*/
public function registerPackage($className, $type)
{
return $this->register($className, $type, AbstractFacetPackage::class);
}
/**
* Get package
*
* @param string $type
* @return AbstractFacetPackage
* @throws InvalidFacetPackageException
*/
public function getPackage($type)
{
$instance = $this->getInstance($type);
if (!$instance instanceof AbstractFacetPackage) {
throw new InvalidFacetPackageException('Invalid class registered for ' . htmlspecialchars($type));
}
$instance->setObjectManager($this->objectManager);
return $instance;
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets;
/***************************************************************
* Copyright notice
*
* (c) 2012-2015 Ingo Renner <ingo@typo3.org>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project is
* free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
/**
* The facet url encode is responsible to encode and decode values for EXT:meilisearch urls.
*
* @author Ingo Renner <ingo@typo3.org>
* @author Timo Hund <timo.hund@dkd.de>
*/
interface FacetUrlDecoderInterface
{
/**
* Parses the query filter from GET parameters in the URL and translates it
* to a Lucene filter value.
*
* @param string $value the filter query from plugin
* @param array $configuration Facet configuration
* @return string Value to be used in a Lucene filter
*/
public function decode($value, array $configuration = []);
}

View File

@@ -0,0 +1,27 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets;
/***************************************************************
* Copyright notice
*
* (c) 2017- Timo Hund <timo.hund@dkd.de>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project is
* free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
class InvalidFacetPackageException extends \Exception {}

View File

@@ -0,0 +1,27 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets;
/***************************************************************
* Copyright notice
*
* (c) 2017- Timo Hund <timo.hund@dkd.de>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project is
* free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
class InvalidFacetParserException extends \Exception {}

View File

@@ -0,0 +1,27 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets;
/***************************************************************
* Copyright notice
*
* (c) 2017- Timo Hund <timo.hund@dkd.de>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project is
* free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
class InvalidQueryBuilderException extends \Exception {}

View File

@@ -0,0 +1,27 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets;
/***************************************************************
* Copyright notice
*
* (c) 2017- Timo Hund <timo.hund@dkd.de>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project is
* free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
class InvalidUrlDecoderException extends \Exception {}

View File

@@ -0,0 +1,69 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\OptionBased;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\AbstractFacet;
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\AbstractFacetItem;
/**
* Base class for all facet items that are represented as option
*
* @author Frans Saris <frans@beech.it>
* @author Timo Hund <timo.hund@dkd.de>
*/
class AbstractOptionFacetItem extends AbstractFacetItem
{
/**
* @var string
*/
protected $value = '';
/**
* @param AbstractFacet $facet
* @param string $label
* @param string $value
* @param int $documentCount
* @param bool $selected
* @param array $metrics
*/
public function __construct(AbstractFacet $facet, $label = '', $value = '', $documentCount = 0, $selected = false, $metrics = [])
{
$this->value = $value;
parent::__construct($facet, $label, $documentCount, $selected, $metrics);
}
/**
* @return string
*/
public function getValue()
{
return $this->value;
}
/**
* @return string
*/
public function getUriValue()
{
return $this->getValue();
}
/**
* @return string
*/
public function getCollectionKey()
{
return $this->getValue();
}
}

View File

@@ -0,0 +1,92 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\OptionBased;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\AbstractFacet;
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\SearchResultSet;
/**
* Class AbstractOptionsFacet
*
* @author Frans Saris <frans@beech.it>
* @author Timo Hund <timo.hund@dkd.de>
*/
class AbstractOptionsFacet extends AbstractFacet
{
/**
* @var OptionCollection
*/
protected $options;
/**
* OptionsFacet constructor
*
* @param SearchResultSet $resultSet
* @param string $name
* @param string $field
* @param string $label
* @param array $configuration Facet configuration passed from typoscript
*/
public function __construct(SearchResultSet $resultSet, $name, $field, $label = '', array $configuration = [])
{
parent::__construct($resultSet, $name, $field, $label, $configuration);
$this->options = new OptionCollection();
}
/**
* @return OptionCollection
*/
public function getOptions()
{
return $this->options;
}
/**
* @param OptionCollection $options
*/
public function setOptions($options)
{
$this->options = $options;
}
/**
* @param AbstractOptionFacetItem $option
*/
public function addOption(AbstractOptionFacetItem $option)
{
$this->options->add($option);
}
/**
* The implementation of this method should return a "flatten" collection of all items.
*
* @return OptionCollection
*/
public function getAllFacetItems()
{
return $this->options;
}
/**
* Get facet partial name used for rendering the facet
*
* @return string
*/
public function getPartialName()
{
return !empty($this->configuration['partialName']) ? $this->configuration['partialName'] : 'Options';
}
}

View File

@@ -0,0 +1,130 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\OptionBased\Hierarchy;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\AbstractFacet;
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\AbstractFacetItemCollection;
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\SearchResultSet;
/**
* Value object that represent a options facet.
*
* @author Frans Saris <frans@beech.it>
* @author Timo Hund <timo.hund@dkd.de>
*/
class HierarchyFacet extends AbstractFacet
{
const TYPE_HIERARCHY = 'hierarchy';
/**
* String
* @var string
*/
protected static $type = self::TYPE_HIERARCHY;
/**
* @var NodeCollection
*/
protected $childNodes;
/**
* @var NodeCollection
*/
protected $allNodes;
/**
* @var array
*/
protected $nodesByKey = [];
/**
* OptionsFacet constructor
*
* @param SearchResultSet $resultSet
* @param string $name
* @param string $field
* @param string $label
* @param array $configuration Facet configuration passed from typoscript
*/
public function __construct(SearchResultSet $resultSet, $name, $field, $label = '', array $configuration = [])
{
parent::__construct($resultSet, $name, $field, $label, $configuration);
$this->childNodes = new NodeCollection();
$this->allNodes = new NodeCollection();
}
/**
* @param Node $node
*/
public function addChildNode(Node $node)
{
$this->childNodes->add($node);
}
/**
* @return NodeCollection
*/
public function getChildNodes()
{
return $this->childNodes;
}
/**
* Creates a new node on the right position with the right parent node.
*
* @param string $parentKey
* @param string $key
* @param string $label
* @param string $value
* @param integer $count
* @param boolean $selected
*/
public function createNode($parentKey, $key, $label, $value, $count, $selected)
{
/** @var $parentNode Node|null */
$parentNode = isset($this->nodesByKey[$parentKey]) ? $this->nodesByKey[$parentKey] : null;
/** @var Node $node */
$node = $this->objectManager->get(Node::class, $this, $parentNode, $key, $label, $value, $count, $selected);
$this->nodesByKey[$key] = $node;
if ($parentNode === null) {
$this->addChildNode($node);
} else {
$parentNode->addChildNode($node);
}
$this->allNodes->add($node);
}
/**
* Get facet partial name used for rendering the facet
*
* @return string
*/
public function getPartialName()
{
return !empty($this->configuration['partialName']) ? $this->configuration['partialName'] : 'Hierarchy';
}
/**
* The implementation of this method should return a "flatten" collection of all items.
*
* @return AbstractFacetItemCollection
*/
public function getAllFacetItems()
{
return $this->allNodes;
}
}

View File

@@ -0,0 +1,159 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\OptionBased\Hierarchy;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\AbstractFacetParser;
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\SearchResultSet;
use WapplerSystems\Meilisearch\System\Solr\ParsingUtil;
/**
* Class HierarchyFacetParser
*/
class HierarchyFacetParser extends AbstractFacetParser
{
/**
* @param SearchResultSet $resultSet
* @param string $facetName
* @param array $facetConfiguration
* @return HierarchyFacet|null
*/
public function parse(SearchResultSet $resultSet, $facetName, array $facetConfiguration)
{
$response = $resultSet->getResponse();
$fieldName = $facetConfiguration['field'];
$label = $this->getPlainLabelOrApplyCObject($facetConfiguration);
$optionsFromSolrResponse = isset($response->facet_counts->facet_fields->{$fieldName}) ? ParsingUtil::getMapArrayFromFlatArray($response->facet_counts->facet_fields->{$fieldName}) : [];
$optionsFromRequest = $this->getActiveFacetValuesFromRequest($resultSet, $facetName);
$hasOptionsInResponse = !empty($optionsFromSolrResponse);
$hasSelectedOptionsInRequest = count($optionsFromRequest) > 0;
$hasNoOptionsToShow = !$hasOptionsInResponse && !$hasSelectedOptionsInRequest;
$hideEmpty = !$resultSet->getUsedSearchRequest()->getContextTypoScriptConfiguration()->getSearchFacetingShowEmptyFacetsByName($facetName);
if ($hasNoOptionsToShow && $hideEmpty) {
return null;
}
/** @var $facet HierarchyFacet */
$facet = $this->objectManager->get(HierarchyFacet::class, $resultSet, $facetName, $fieldName, $label, $facetConfiguration);
$hasActiveOptions = count($optionsFromRequest) > 0;
$facet->setIsUsed($hasActiveOptions);
$facet->setIsAvailable($hasOptionsInResponse);
$nodesToCreate = $this->getMergedFacetValueFromSearchRequestAndSolrResponse($optionsFromSolrResponse, $optionsFromRequest);
if ($this->facetOptionsMustBeResorted($facetConfiguration)) {
$nodesToCreate = $this->sortFacetOptionsInNaturalOrder($nodesToCreate);
}
foreach ($nodesToCreate as $value => $count) {
if ($this->getIsExcludedFacetValue($value, $facetConfiguration)) {
continue;
}
$isActive = in_array($value, $optionsFromRequest);
$delimiterPosition = strpos($value, '-');
$path = substr($value, $delimiterPosition + 1);
$pathArray = $this->getPathAsArray($path);
$key = array_pop($pathArray);
$parentKey = array_pop($pathArray);
$value = '/' . $path;
$label = $this->getLabelFromRenderingInstructions($key, $count, $facetName, $facetConfiguration);
$facet->createNode($parentKey, $key, $label, $value, $count, $isActive);
}
return $facet;
}
/**
* Sorts facet options in natural order.
* Options must be sorted in natural order,
* because lower nesting levels must be instantiated first, to serve as parents for higher nested levels.
* See implementation of HierarchyFacet::createNode().
*
* @param array $flatOptionsListForFacet
* @return void sorted list of facet options
*/
protected function sortFacetOptionsInNaturalOrder(array $flatOptionsListForHierarchyFacet)
{
uksort($flatOptionsListForHierarchyFacet, "strnatcmp");
return $flatOptionsListForHierarchyFacet;
}
/**
* Checks if options must be resorted.
*
* Apache Solr facet.sort can be set globally or per facet.
* Relevant TypoScript paths:
* plugin.tx_meilisearch.search.faceting.sortBy causes facet.sort Apache Solr parameter
* plugin.tx_meilisearch.search.faceting.facets.[facetName].sortBy causes f.<fieldname>.facet.sort parameter
*
* see: https://lucene.apache.org/solr/guide/6_6/faceting.html#Faceting-Thefacet.sortParameter
* see: https://wiki.apache.org/solr/SimpleFacetParameters#facet.sort : "This parameter can be specified on a per field basis."
*
* @param array $facetConfiguration
* @return bool
*/
protected function facetOptionsMustBeResorted(array $facetConfiguration)
{
if (isset($facetConfiguration['sortBy']) && $facetConfiguration['sortBy'] === 'index') {
return true;
}
return false;
}
/**
* This method is used to get the path array from a hierarchical facet. It substitutes escaped slashes to keep them
* when they are used inside a facetValue.
*
* @param string $path
* @return array
*/
protected function getPathAsArray($path)
{
$path = str_replace('\/', '@@@', $path);
$path = rtrim($path, "/");
$segments = explode('/', $path);
return array_map(function($item) {
return str_replace('@@@', '/', $item);
}, $segments);
}
/**
* Retrieves the active facetValue for a facet from the search request.
* @param SearchResultSet $resultSet
* @param string $facetName
* @return array
*/
protected function getActiveFacetValuesFromRequest(SearchResultSet $resultSet, $facetName)
{
$activeFacetValues = [];
$values = $resultSet->getUsedSearchRequest()->getActiveFacetValuesByName($facetName);
foreach (is_array($values) ? $values : [] as $valueFromRequest) {
// Attach the 'depth' param again to the value
if (strpos($valueFromRequest, '-') === false) {
$valueFromRequest = HierarchyTool::substituteSlashes($valueFromRequest);
$valueFromRequest = trim($valueFromRequest, '/');
$valueFromRequest = (count(explode('/', $valueFromRequest)) - 1) . '-' . $valueFromRequest . '/';
$valueFromRequest = HierarchyTool::unSubstituteSlashes($valueFromRequest);
}
$activeFacetValues[] = $valueFromRequest;
}
return $activeFacetValues;
}
}

View File

@@ -0,0 +1,37 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\OptionBased\Hierarchy;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\AbstractFacetPackage;
/**
* Class HierarchyPackage
*/
class HierarchyPackage extends AbstractFacetPackage {
/**
* @return string
*/
public function getParserClassName() {
return (string)HierarchyFacetParser::class;
}
/**
* @return string
*/
public function getUrlDecoderClassName() {
return (string)HierarchyUrlDecoder::class;
}
}

View File

@@ -0,0 +1,41 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\OptionBased\Hierarchy;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
class HierarchyTool
{
/**
* Replaces all escaped slashes in a hierarchy path with @@@slash@@@ to afterwards
* only have slashes in the content that are real path separators.
*
* @param string $pathWithContentSlashes
* @return string
*/
public static function substituteSlashes(string $pathWithContentSlashes): string
{
return (string)str_replace('\/', '@@@slash@@@', $pathWithContentSlashes);
}
/**
* Replaces @@@slash@@@ with \/ to have the path usable for solr again.
*
* @param string $pathWithReplacedContentSlashes
* @return string
*/
public static function unSubstituteSlashes(string $pathWithReplacedContentSlashes): string
{
return (string)str_replace('@@@slash@@@', '\/', $pathWithReplacedContentSlashes);
}
}

View File

@@ -0,0 +1,64 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\OptionBased\Hierarchy;
/***************************************************************
* Copyright notice
*
* (c) 2012-2015 Ingo Renner <ingo@typo3.org>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project is
* free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\FacetUrlDecoderInterface;
/**
* Filter encoder to build Solr hierarchy queries from tx_meilisearch[filter]
*
* @author Ingo Renner <ingo@typo3.org>
*/
class HierarchyUrlDecoder implements FacetUrlDecoderInterface
{
/**
* Delimiter for hierarchies in the URL.
*
* @var string
*/
const DELIMITER = '/';
/**
* Parses the given hierarchy filter and returns a Solr filter query.
*
* @param string $hierarchy The hierarchy filter query.
* @param array $configuration Facet configuration
* @return string Lucene query language filter to be used for querying Solr
*/
public function decode($hierarchy, array $configuration = [])
{
$escapedHierarchy = HierarchyTool::substituteSlashes($hierarchy);
$escapedHierarchy = substr($escapedHierarchy, 1);
$escapedHierarchy = rtrim($escapedHierarchy, '/');
$hierarchyItems = explode(self::DELIMITER, $escapedHierarchy);
$filterContent = (count($hierarchyItems) - 1) . '-' . $escapedHierarchy . '/';
$filterContent = HierarchyTool::unSubstituteSlashes($filterContent);
return '"' . str_replace("\\", "\\\\", $filterContent) . '"';
}
}

View File

@@ -0,0 +1,119 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\OptionBased\Hierarchy;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\OptionBased\AbstractOptionFacetItem;
/**
* Value object that represent an option of a options facet.
*
* @author Frans Saris <frans@beech.it>
* @author Timo Hund <timo.hund@dkd.de>
*/
class Node extends AbstractOptionFacetItem
{
/**
* @var NodeCollection
*/
protected $childNodes;
/**
* @var Node
*/
protected $parentNode;
/**
* @var integer
*/
protected $depth;
/**
* @var string
*/
protected $key;
/**
* @param HierarchyFacet $facet
* @param Node $parentNode
* @param string $key
* @param string $label
* @param string $value
* @param int $documentCount
* @param bool $selected
*/
public function __construct(HierarchyFacet $facet, $parentNode = null, $key = '', $label = '', $value = '', $documentCount = 0, $selected = false)
{
parent::__construct($facet, $label, $value, $documentCount, $selected);
$this->value = $value;
$this->childNodes = new NodeCollection();
$this->parentNode = $parentNode;
$this->key = $key;
}
/**
* @return string
*/
public function getKey()
{
return $this->key;
}
/**
* @param Node $node
*/
public function addChildNode(Node $node)
{
$this->childNodes->add($node);
}
/**
* @return NodeCollection
*/
public function getChildNodes()
{
return $this->childNodes;
}
/**
* @return Node|null
*/
public function getParentNode()
{
return $this->parentNode;
}
/**
* @return bool
*/
public function getHasParentNode()
{
return $this->parentNode !== null;
}
/**
* @return bool
*/
public function getHasChildNodeSelected()
{
/** @var Node $childNode */
foreach ($this->childNodes as $childNode) {
if ($childNode->getSelected()) {
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\OptionBased\Hierarchy;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\AbstractFacetItemCollection;
/**
* Collection for facet options.
*
* @author Frans Saris <frans@beech.it>
* @author Timo Hund <timo.hund@dkd.de>
*/
class NodeCollection extends AbstractFacetItemCollection
{
/**
* @param Node $node
* @return NodeCollection
*/
public function add($node)
{
return parent::add($node);
}
/**
* @param int $position
* @return Node
*/
public function getByPosition($position)
{
return parent::getByPosition($position);
}
}

View File

@@ -0,0 +1,75 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\OptionBased;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\AbstractFacetItemCollection;
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\OptionBased\Options\Option;
/**
* Collection for facet options.
*
* @author Frans Saris <frans@beech.it>
* @author Timo Hund <timo.hund@dkd.de>
*/
class OptionCollection extends AbstractFacetItemCollection
{
/**
* Returns an array of prefixes from the option labels.
*
* Red, Blue, Green => r, g, b
*
* Can be used in combination with getByPrefix() to group facet options by prefix (e.g. alphabetical).
*
* @param int $length
* @return array
*/
public function getLowercaseLabelPrefixes($length = 1)
{
$prefixes = $this->getLabelPrefixes($length);
return array_map('mb_strtolower', $prefixes);
}
/**
* @param string $filteredPrefix
* @return AbstractFacetItemCollection
*/
public function getByLowercaseLabelPrefix($filteredPrefix)
{
return $this->getFilteredCopy(function(Option $option) use ($filteredPrefix)
{
$filteredPrefixLength = mb_strlen($filteredPrefix);
$currentPrefix = mb_substr(mb_strtolower($option->getLabel()), 0, $filteredPrefixLength);
return $currentPrefix === $filteredPrefix;
});
}
/**
* @param int $length
* @return array
*/
protected function getLabelPrefixes($length = 1) : array
{
$prefixes = [];
foreach ($this->data as $option) {
/** @var $option Option */
$prefix = mb_substr($option->getLabel(), 0, $length);
$prefixes[$prefix] = $prefix;
}
return array_values($prefixes);
}
}

Some files were not shown because too many files have changed in this diff Show More