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