From 5525d81e6878cc34efbf680ba191ccf05c64ed3a Mon Sep 17 00:00:00 2001 From: Oliver Goldschmidt <o.goldschmidt@tuhh.de> Date: Fri, 24 Apr 2015 09:45:20 -0400 Subject: [PATCH] enables conditional filters for Solr --- config/vufind/permissions.ini | 10 +- config/vufind/searches.ini | 21 ++ .../Factory/AbstractSolrBackendFactory.php | 26 ++ .../Solr/InjectConditionalFilterListener.php | 147 ++++++++ .../Solr/ConditionalFilterListenerTest.php | 321 ++++++++++++++++++ 5 files changed, 524 insertions(+), 1 deletion(-) create mode 100644 module/VuFind/src/VuFind/Search/Solr/InjectConditionalFilterListener.php create mode 100644 module/VuFind/tests/unit-tests/src/VuFindTest/Search/Solr/ConditionalFilterListenerTest.php diff --git a/config/vufind/permissions.ini b/config/vufind/permissions.ini index 875772f156c..b7e68bdf34f 100644 --- a/config/vufind/permissions.ini +++ b/config/vufind/permissions.ini @@ -91,4 +91,12 @@ permission = access.StaffViewTab ; Only users with a staff affiliation can access the staff view tab ;[shibboleth.StaffView] ;shibboleth = "affiliation staff@example.org" -;permission = access.StaffViewTab \ No newline at end of file +;permission = access.StaffViewTab + +; Example for conditional filters (see [ConditionalHiddenFilters] in +; searches.ini for details) +;[conditionalFilter.MyUniversity] +;require = ANY +;ipRange[] = 1.2.3.1-1.2.3.254 +;role = loggedin +;permission = conditionalFilter.MyUniversity diff --git a/config/vufind/searches.ini b/config/vufind/searches.ini index 3b99fce01db..72933256341 100644 --- a/config/vufind/searches.ini +++ b/config/vufind/searches.ini @@ -547,6 +547,27 @@ container_title = "Journal Title" ;0 = "format:\"Book\" OR format:\"Journal\"" ;1 = "language:\"English\" OR language:\"French\"" +; This section can get used to define conditional filters, i.e. filters +; that are applied under certain conditions. +; You can use a permission set as condition, which has to be defined in +; permissions.ini. +; Keys are ignored, but increasing numeric values (1, 2, 3...) are recommended. +; Values need to be formatted using this schema: +; [-]permission|filter-query +; Prefixing the condition with a minus (-) means that the filter is applied +; when the condition does not match (the permission is not granted). +; The filter may be any filter query valid for Solr. +; Examples: +; -conditionalFilter.MyUniversity|format:Book +; apply filter "format:Book" if permission conditionalFilter.MyUniversity +; (from permissions.ini) is not granted +; conditionalFilter.MyUniversity|format:Article +; apply filter "format:Article" if permission conditionalFilter.MyUniversity +; (from permissions.ini) is granted +[ConditionalHiddenFilters] +;0 = "-conditionalFilter.MyUniversity|format:Book" +;1 = "conditionalFilter.MyUniversity|format:Article" + ; This section defines how records are handled when being fetched from Solr. [Records] ; Boolean value indicating if deduplication is enabled. Defaults to false. diff --git a/module/VuFind/src/VuFind/Search/Factory/AbstractSolrBackendFactory.php b/module/VuFind/src/VuFind/Search/Factory/AbstractSolrBackendFactory.php index c861f67bfd9..ee92c5cfeed 100644 --- a/module/VuFind/src/VuFind/Search/Factory/AbstractSolrBackendFactory.php +++ b/module/VuFind/src/VuFind/Search/Factory/AbstractSolrBackendFactory.php @@ -29,6 +29,7 @@ namespace VuFind\Search\Factory; use VuFind\Search\Solr\InjectHighlightingListener; +use VuFind\Search\Solr\InjectConditionalFilterListener; use VuFind\Search\Solr\InjectSpellingListener; use VuFind\Search\Solr\MultiIndexListener; use VuFind\Search\Solr\V3\ErrorListener as LegacyErrorListener; @@ -177,6 +178,13 @@ abstract class AbstractSolrBackendFactory implements FactoryInterface // Highlighting $this->getInjectHighlightingListener($backend, $search)->attach($events); + // Conditional Filters + if (isset($search->ConditionalHiddenFilters) + && $search->ConditionalHiddenFilters->count() > 0 + ) { + $this->getInjectConditionalFilterListener($search)->attach($events); + } + // Spellcheck if (isset($config->Spelling->enabled) && $config->Spelling->enabled) { if (isset($config->Spelling->simple) && $config->Spelling->simple) { @@ -395,4 +403,22 @@ abstract class AbstractSolrBackendFactory implements FactoryInterface ? $search->General->highlighting_fields : '*'; return new InjectHighlightingListener($backend, $fl); } + + /** + * Get a Conditional Filter Listener + * + * @param Config $search Search configuration + * + * @return InjectConditionalFilterListener + */ + protected function getInjectConditionalFilterListener(Config $search) + { + $listener = new InjectConditionalFilterListener( + $search->ConditionalHiddenFilters->toArray() + ); + $listener->setAuthorizationService( + $this->serviceLocator->get('ZfcRbac\Service\AuthorizationService') + ); + return $listener; + } } \ No newline at end of file diff --git a/module/VuFind/src/VuFind/Search/Solr/InjectConditionalFilterListener.php b/module/VuFind/src/VuFind/Search/Solr/InjectConditionalFilterListener.php new file mode 100644 index 00000000000..74c69d3c0bf --- /dev/null +++ b/module/VuFind/src/VuFind/Search/Solr/InjectConditionalFilterListener.php @@ -0,0 +1,147 @@ +<?php + +/** + * Conditional Filter listener. + * + * PHP version 5 + * + * Copyright (C) Villanova University 2013. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program 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. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * @category VuFind2 + * @package Search + * @author Oliver Goldschmidt <o.goldschmidt@tuhh.de> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link http://vufind.org Main Site + */ +namespace VuFind\Search\Solr; + +use Zend\EventManager\SharedEventManagerInterface; +use Zend\EventManager\EventInterface; + +use ZfcRbac\Service\AuthorizationServiceAwareInterface, + ZfcRbac\Service\AuthorizationServiceAwareTrait; + +/** + * Conditional Filter listener. + * + * @category VuFind2 + * @package Search + * @author Oliver Goldschmidt <o.goldschmidt@tuhh.de> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link http://vufind.org Main Site + */ +class InjectConditionalFilterListener +{ + use AuthorizationServiceAwareTrait; + + /** + * Filters to apply. + * + * @var array + */ + protected $filterList; + + /** + * Filters from configuration. + * + * @var array + */ + protected $filters; + + /** + * Constructor. + * + * @param array $searchConf Search configuration parameters + * + * @return void + */ + public function __construct($searchConf) + { + $this->filters = $searchConf; + $this->filterList = []; + } + + /** + * Attach listener to shared event manager. + * + * @param SharedEventManagerInterface $manager Shared event manager + * + * @return void + */ + public function attach(SharedEventManagerInterface $manager) + { + $manager->attach('VuFind\Search', 'pre', [$this, 'onSearchPre']); + } + + /** + * Add a conditional filter. + * + * @param String $configOption Conditional Filter + * + * @return void + */ + protected function addConditionalFilter($configOption) + { + $filterArr = explode('|', $configOption); + $filterCondition = $filterArr[0]; + $filter = $filterArr[1]; + $authService = $this->getAuthorizationService(); + + // if no authorization service is available, don't do anything + if (!$authService) { + return; + } + + // if the filter condition starts with a minus (-), it should not match + // to get the filter applied + if (substr($filterCondition, 0, 1) == '-') { + if (!$authService->isGranted(substr($filterCondition, 1))) { + $this->filterList[] = $filter; + } + } else { + // otherwise the condition should match to apply the filter + if ($authService->isGranted($filterCondition)) { + $this->filterList[] = $filter; + } + } + } + + /** + * Set up conditional hidden filters. + * + * @param EventInterface $event Event + * + * @return EventInterface + */ + public function onSearchPre(EventInterface $event) + { + // Add conditional filters + foreach ($this->filters as $fc) { + $this->addConditionalFilter($fc); + } + + $params = $event->getParam('params'); + $fq = $params->get('fq'); + if (!is_array($fq)) { + $fq = []; + } + $new_fq = array_merge($fq, $this->filterList); + $params->set('fq', $new_fq); + + return $event; + } + +} diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Search/Solr/ConditionalFilterListenerTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Search/Solr/ConditionalFilterListenerTest.php new file mode 100644 index 00000000000..576210b25bf --- /dev/null +++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Search/Solr/ConditionalFilterListenerTest.php @@ -0,0 +1,321 @@ +<?php + +/** + * Unit tests for Conditional Filter listener. + * + * PHP version 5 + * + * Copyright (C) Villanova University 2015. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program 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. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * @category VuFind2 + * @package Search + * @author Oliver Goldschmidt <o.goldschmidt@tuhh.de> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link http://vufind.org Main Site + */ +namespace VuFindTest\Search\Solr; + +use VuFindSearch\ParamBag; +use VuFindSearch\Backend\Solr\Backend; +use VuFindSearch\Backend\Solr\Connector; +use VuFindSearch\Backend\Solr\HandlerMap; + + +use VuFind\Search\Solr\InjectConditionalFilterListener; +use VuFindTest\Unit\TestCase; +use Zend\EventManager\Event; + +use ZfcRbac\Service\AuthorizationServiceAwareInterface, + ZfcRbac\Service\AuthorizationServiceAwareTrait; + +/** + * Unit tests for Conditional Filter listener. + * + * @category VuFind2 + * @package Search + * @author Oliver Goldschmidt <o.goldschmidt@tuhh.de> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link http://vufind.org Main Site + */ +class ConditionalFilterListenerTest extends TestCase +{ + /** + * Sample configuration for ConditionalFilters. + * + * @var array + */ + protected static $searchConfig = [ + '0' => '-conditionalFilter.sample|(NOT institution:"MyInst")', + '1' => 'conditionalFilter.sample|institution:"MyInst"' + ]; + + /** + * Sample configuration for empty ConditionalFilters config. + * + * @var array + */ + protected static $emptySearchConfig = [ ]; + + /** + * Backend. + * + * @var BackendInterface + */ + protected $backend; + + /** + * Setup. + * + * @return void + */ + protected function setup() + { + $handlermap = new HandlerMap(['select' => ['fallback' => true]]); + $connector = new Connector('http://example.org/', $handlermap); + $this->backend = new Backend($connector); + } + + /** + * Test attaching listener. + * + * @return void + */ + public function testAttach() + { + $listener = new InjectConditionalFilterListener(self::$emptySearchConfig); + $mock = $this->getMock('Zend\EventManager\SharedEventManagerInterface'); + $mock->expects($this->once())->method('attach')->with( + $this->equalTo('VuFind\Search'), + $this->equalTo('pre'), + $this->equalTo([$listener, 'onSearchPre']) + ); + $listener->attach($mock); + } + + /** + * Test the listener without setting an authorization service. + * This should return an empty array. + * + * @return void + */ + public function testConditionalFilterWithoutAuthorizationService() + { + $params = new ParamBag([ ]); + $listener = new InjectConditionalFilterListener(self::$searchConfig); + + $event = new Event('pre', $this->backend, [ 'params' => $params]); + $listener->onSearchPre($event); + + $fq = $params->get('fq'); + $this->assertEquals([ ], $fq); + } + + /** + * Test the listener without setting an authorization service, + * but with fq-parameters. + * This should not touch the parameters. + * + * @return void + */ + public function testConditionalFilterWithoutAuthorizationServiceWithParams() + { + $params = new ParamBag( + [ + 'fq' => ['fulltext:Vufind', 'field2:novalue'], + ] + ); + $listener = new InjectConditionalFilterListener(self::$searchConfig); + + $event = new Event('pre', $this->backend, [ 'params' => $params]); + $listener->onSearchPre($event); + + $fq = $params->get('fq'); + $this->assertEquals( + [0 => 'fulltext:Vufind', + 1 => 'field2:novalue'], $fq + ); + } + + /** + * Test the listener with an empty conditional filter config. + * + * @return void + */ + public function testConditionalFilterEmptyConfig() + { + $params = new ParamBag([ ]); + $listener = new InjectConditionalFilterListener(self::$emptySearchConfig); + $mockAuth = $this->getMockBuilder('ZfcRbac\Service\AuthorizationService') + ->disableOriginalConstructor() + ->getMock(); + $listener->setAuthorizationService($mockAuth); + + $event = new Event('pre', $this->backend, [ 'params' => $params]); + $listener->onSearchPre($event); + + $fq = $params->get('fq'); + $this->assertEquals([ ], $fq); + } + + /** + * Test the listener with an empty conditional filter config, + * but with given fq parameters + * + * @return void + */ + public function testConditionalFilterEmptyConfigWithFQ() + { + $params = new ParamBag( + [ + 'fq' => ['fulltext:Vufind', 'field2:novalue'], + ] + ); + $listener = new InjectConditionalFilterListener(self::$emptySearchConfig); + $mockAuth = $this->getMockBuilder('ZfcRbac\Service\AuthorizationService') + ->disableOriginalConstructor() + ->getMock(); + $listener->setAuthorizationService($mockAuth); + + $event = new Event('pre', $this->backend, [ 'params' => $params]); + $listener->onSearchPre($event); + + $fq = $params->get('fq'); + $this->assertEquals( + [0 => 'fulltext:Vufind', + 1 => 'field2:novalue'], $fq + ); + } + + /** + * Test the listener without preset fq parameters + * if the conditional filter is granted + * + * @return void + */ + public function testConditionalFilter() + { + $params = new ParamBag([ ]); + $listener = new InjectConditionalFilterListener(self::$searchConfig); + $mockAuth = $this->getMockBuilder('ZfcRbac\Service\AuthorizationService') + ->disableOriginalConstructor() + ->getMock(); + $mockAuth->expects($this->any())->method('isGranted') + ->with($this->equalTo('conditionalFilter.sample')) + ->will($this->returnValue(true)); + $listener->setAuthorizationService($mockAuth); + + $event = new Event('pre', $this->backend, [ 'params' => $params]); + $listener->onSearchPre($event); + + $fq = $params->get('fq'); + $this->assertEquals( + [0 => 'institution:"MyInst"'], $fq + ); + } + + /** + * Test the listener without preset fq parameters + * if the conditional filter is not granted + * + * @return void + */ + public function testNegativeConditionalFilter() + { + $params = new ParamBag([ ]); + + $listener = new InjectConditionalFilterListener(self::$searchConfig); + $mockAuth = $this->getMockBuilder('ZfcRbac\Service\AuthorizationService') + ->disableOriginalConstructor() + ->getMock(); + $mockAuth->expects($this->any())->method('isGranted') + ->with($this->equalTo('conditionalFilter.sample')) + ->will($this->returnValue(false)); + $listener->setAuthorizationService($mockAuth); + $event = new Event('pre', $this->backend, [ 'params' => $params ]); + $listener->onSearchPre($event); + + $fq = $params->get('fq'); + $this->assertEquals([0 => '(NOT institution:"MyInst")'], $fq); + } + + /** + * Test the listener with preset fq-parameters + * if the conditional filter is not granted + * + * @return void + */ + public function testNegativeConditionalFilterWithFQ() + { + $params = new ParamBag( + [ + 'fq' => ['fulltext:Vufind', 'field2:novalue'], + ] + ); + + $listener = new InjectConditionalFilterListener(self::$searchConfig); + $mockAuth = $this->getMockBuilder('ZfcRbac\Service\AuthorizationService') + ->disableOriginalConstructor() + ->getMock(); + $mockAuth->expects($this->any())->method('isGranted') + ->with($this->equalTo('conditionalFilter.sample')) + ->will($this->returnValue(false)); + $listener->setAuthorizationService($mockAuth); + $event = new Event('pre', $this->backend, ['params' => $params]); + $listener->onSearchPre($event); + + $fq = $params->get('fq'); + $this->assertEquals( + [0 => 'fulltext:Vufind', + 1 => 'field2:novalue', + 2 => '(NOT institution:"MyInst")' + ], $fq + ); + } + + /** + * Test the listener with preset fq-parameters + * if the conditional filter is granted + * + * @return void + */ + public function testConditionalFilterWithFQ() + { + $params = new ParamBag( + [ + 'fq' => ['fulltext:Vufind', 'field2:novalue'], + ] + ); + + $listener = new InjectConditionalFilterListener(self::$searchConfig); + $mockAuth = $this->getMockBuilder('ZfcRbac\Service\AuthorizationService') + ->disableOriginalConstructor() + ->getMock(); + $mockAuth->expects($this->any())->method('isGranted') + ->with($this->equalTo('conditionalFilter.sample')) + ->will($this->returnValue(true)); + $listener->setAuthorizationService($mockAuth); + $event = new Event('pre', $this->backend, ['params' => $params]); + $listener->onSearchPre($event); + + $fq = $params->get('fq'); + $this->assertEquals( + [0 => 'fulltext:Vufind', + 1 => 'field2:novalue', + 2 => 'institution:"MyInst"' + ], $fq + ); + } +} -- GitLab