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