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

30
.editorconfig Normal file
View File

@ -0,0 +1,30 @@
# EditorConfig is awesome: http://EditorConfig.org
# TYPO3 Standard: https://github.com/TYPO3/TYPO3.CMS/blob/master/.editorconfig
# top-most EditorConfig file
root = true
[*]
charset = utf-8
end_of_line = lf
indent_style = space
indent_size = 4
insert_final_newline = true
trim_trailing_whitespace = true
# ReST-Files
[*.rst]
indent_size = 3
max_line_length = 80
# YAML-Files
[*.{yaml,yml}]
indent_size = 2
# TypoScript
[*.{typoscript,tsconfig}]
indent_size = 2
# XLF/XML-Files
[*.{xlf,xml}]
indent_style = tab

1
.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1 @@
custom: ["https://wappler.systems/"]

35
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@ -0,0 +1,35 @@
---
name: Bug report
about: Create a report to help us improve
title: "[BUG] Please add a speaking title"
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Used versions (please complete the following information):**
- TYPO3 Version: [e.g. 10.4.13]
- Browser: [e.g. chrome, safari]
- EXT:meilisearch Version: [e.g. 10.0.0]
- Used Meilisearch Version: [e.g. 8.8.0]
- PHP Version: [e.g. 7.4.0]
- MySQL Version: [e.g. 8.0.0]
**Additional context**
Add any other context about the problem here.

View File

@ -0,0 +1,23 @@
---
name: Feature request
about: Suggest an idea for this project
title: "[FEATURE] Please describe your feature wish here"
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
**Target versions**
Please add the EXT:meilisearch target versions here.

10
.github/ISSUE_TEMPLATE/task.md vendored Normal file
View File

@ -0,0 +1,10 @@
---
name: Task
about: This template is used to decribe regular tasks
title: "[TASK]"
labels: ''
assignees: ''
---
**What should be done in the scope of this task?**

14
.github/no-response.yml vendored Normal file
View File

@ -0,0 +1,14 @@
# Configuration for probot-no-response - https://github.com/probot/no-response
# Number of days of inactivity before an Issue is closed for lack of response
daysUntilClose: 28
# Label requiring a response
responseRequiredLabel: more-information-needed
# Comment to post when closing an Issue for lack of response. Set to `false` to disable
closeComment: >
This issue has been automatically closed because there has been no response
to our request for more information from the original author. With only the
information that is currently in the issue, we don't have enough information
to take action. Please reach out if you have or find the answers we need so
that we can investigate further.

18
.gitignore vendored Normal file
View File

@ -0,0 +1,18 @@
.buildpath
.project/
.settings/
.idea/
atlassian-ide-plugin.xml
.DS_Store
index.php
typo3
typo3_src
typo3conf
typo3temp
uploads
vendor
/composer.lock
.Build
Documentation/_make
.php_cs.cache

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;
}
}

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