From 4e9f061fadd5b1f5dc6e3d8b25d2c638c2d36b1c Mon Sep 17 00:00:00 2001
From: Demian Katz <demian.katz@villanova.edu>
Date: Wed, 2 Mar 2016 10:23:48 -0500
Subject: [PATCH] Fix for PostgreSQL bytea write bug. - This enables PDO to
 properly escape data sent to the bytea search_object field. - Includes test
 scenario to prevent regressions.

---
 module/VuFind/src/VuFind/Db/Table/Gateway.php |  4 +-
 module/VuFind/src/VuFind/Db/Table/Search.php  | 49 +++++++++++++++++++
 .../src/VuFindTest/Mink/SearchActionsTest.php | 42 ++++++++++------
 3 files changed, 80 insertions(+), 15 deletions(-)

diff --git a/module/VuFind/src/VuFind/Db/Table/Gateway.php b/module/VuFind/src/VuFind/Db/Table/Gateway.php
index 6e75046e0c1..f8a17dc2a2c 100644
--- a/module/VuFind/src/VuFind/Db/Table/Gateway.php
+++ b/module/VuFind/src/VuFind/Db/Table/Gateway.php
@@ -93,7 +93,9 @@ class Gateway extends AbstractTableGateway implements ServiceLocatorAwareInterfa
             $maps = isset($cfg['vufind']['pgsql_seq_mapping'])
                 ? $cfg['vufind']['pgsql_seq_mapping'] : null;
             if (isset($maps[$this->table])) {
-                $this->featureSet = new Feature\FeatureSet();
+                if (!is_object($this->featureSet)) {
+                    $this->featureSet = new Feature\FeatureSet();
+                }
                 $this->featureSet->addFeature(
                     new Feature\SequenceFeature(
                         $maps[$this->table][0], $maps[$this->table][1]
diff --git a/module/VuFind/src/VuFind/Db/Table/Search.php b/module/VuFind/src/VuFind/Db/Table/Search.php
index 91f3c312a12..fe2353a9a2a 100644
--- a/module/VuFind/src/VuFind/Db/Table/Search.php
+++ b/module/VuFind/src/VuFind/Db/Table/Search.php
@@ -27,6 +27,8 @@
  */
 namespace VuFind\Db\Table;
 use minSO;
+use Zend\Db\Adapter\ParameterContainer;
+use Zend\Db\TableGateway\Feature;
 
 /**
  * Table Definition for search
@@ -47,6 +49,53 @@ class Search extends Gateway
         parent::__construct('search', 'VuFind\Db\Row\Search');
     }
 
+    /**
+     * Initialize
+     *
+     * @return void
+     */
+    public function initialize()
+    {
+        if ($this->isInitialized) {
+            return;
+        }
+
+        // Special case for PostgreSQL inserts -- we need to provide an extra
+        // clue so that the database knows how to write bytea data correctly:
+        if ($this->adapter->getDriver()->getDatabasePlatformName() == "Postgresql") {
+            if (!is_object($this->featureSet)) {
+                $this->featureSet = new Feature\FeatureSet();
+            }
+            $eventFeature = new Feature\EventFeature();
+            $eventFeature->getEventManager()->attach(
+                Feature\EventFeature::EVENT_PRE_INSERT, [$this, 'onPreInsert']
+            );
+            $this->featureSet->addFeature($eventFeature);
+        }
+
+        parent::initialize();
+    }
+
+    /**
+     * Customize the Insert object to include extra metadata about the
+     * search_object field so that it will be written correctly. This is
+     * triggered only when we're interacting with PostgreSQL; MySQL works fine
+     * without the extra hint.
+     *
+     * @param object $event Event object
+     *
+     * @return void
+     */
+    public function onPreInsert($event)
+    {
+        $driver = $event->getTarget()->getAdapter()->getDriver();
+        $statement = $driver->createStatement();
+        $params = new ParameterContainer();
+        $params->offsetSetErrata('search_object', ParameterContainer::TYPE_LOB);
+        $statement->setParameterContainer($params);
+        $driver->registerStatementPrototype($statement);
+    }
+
     /**
      * Delete unsaved searches for a particular session.
      *
diff --git a/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/SearchActionsTest.php b/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/SearchActionsTest.php
index bbeaef71b64..13a298c798e 100644
--- a/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/SearchActionsTest.php
+++ b/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/SearchActionsTest.php
@@ -109,12 +109,18 @@ class SearchActionsTest extends \VuFindTest\Unit\MinkTestCase
      */
     public function testSearchHistory()
     {
-        $page = $this->performSearch('foo');
-        $page->findLink('Search History')->click();
+        // Use "foo \ bar" as our search because the backslash has been known
+        // to cause problems in some situations (e.g. PostgreSQL database with
+        // incorrect escaping); this allows us to catch regressions for a few
+        // different problems in a single test.
+        $page = $this->performSearch('foo \ bar');
+        $this->findAndAssertLink($page, 'Search History')->click();
         $this->snooze();
-        // We should see our "foo" search in the history, but no saved
+        // We should see our "foo \ bar" search in the history, but no saved
         // searches because we are logged out:
-        $this->assertEquals('foo', $page->findLink('foo')->getText());
+        $this->assertEquals(
+            'foo \ bar', $this->findAndAssertLink($page, 'foo \ bar')->getText()
+        );
         $this->assertFalse(
             $this->hasElementsMatchingText($page, 'h2', 'Saved Searches')
         );
@@ -126,18 +132,24 @@ class SearchActionsTest extends \VuFindTest\Unit\MinkTestCase
         $this->snooze();
         $this->fillInLoginForm($page, 'username1', 'test');
         $this->submitLoginForm($page);
-        $this->assertEquals('foo', $page->findLink('foo')->getText());
+        $this->assertEquals(
+            'foo \ bar', $this->findAndAssertLink($page, 'foo \ bar')->getText()
+        );
         $this->assertTrue(
             $this->hasElementsMatchingText($page, 'h2', 'Saved Searches')
         );
-        $this->assertEquals('test', $page->findLink('test')->getText());
+        $this->assertEquals(
+            'test', $this->findAndAssertLink($page, 'test')->getText()
+        );
 
         // Now purge unsaved searches, confirm that unsaved search is gone
         // but saved search is still present:
-        $page->findLink('Purge unsaved searches')->click();
+        $this->findAndAssertLink($page, 'Purge unsaved searches')->click();
         $this->snooze();
-        $this->assertNull($page->findLink('foo'));
-        $this->assertEquals('test', $page->findLink('test')->getText());
+        $this->assertNull($page->findLink('foo \ bar'));
+        $this->assertEquals(
+            'test', $this->findAndAssertLink($page, 'test')->getText()
+        );
     }
 
     /**
@@ -155,8 +167,8 @@ class SearchActionsTest extends \VuFindTest\Unit\MinkTestCase
         $this->snooze();
         $this->fillInLoginForm($page, 'username1', 'test');
         $this->submitLoginForm($page);
-        $delete = $page->findLink('Delete')->getAttribute('href');
-        $page->findLink('Log Out')->click();
+        $delete = $this->findAndAssertLink($page, 'Delete')->getAttribute('href');
+        $this->findAndAssertLink($page, 'Log Out')->click();
         $this->snooze();
 
         // Use user A's delete link, but try to execute it as user B:
@@ -170,11 +182,11 @@ class SearchActionsTest extends \VuFindTest\Unit\MinkTestCase
         );
         $this->findCss($page, 'input.btn.btn-primary')->click();
         $this->snooze();
-        $page->findLink('Log Out')->click();
+        $this->findAndAssertLink($page, 'Log Out')->click();
         $this->snooze();
 
         // Go back in as user A -- see if the saved search still exists.
-        $page->findLink('Search History')->click();
+        $this->findAndAssertLink($page, 'Search History')->click();
         $this->snooze();
         $this->findCss($page, '#loginOptions a')->click();
         $this->snooze();
@@ -183,7 +195,9 @@ class SearchActionsTest extends \VuFindTest\Unit\MinkTestCase
         $this->assertTrue(
             $this->hasElementsMatchingText($page, 'h2', 'Saved Searches')
         );
-        $this->assertEquals('test', $page->findLink('test')->getText());
+        $this->assertEquals(
+            'test', $this->findAndAssertLink($page, 'test')->getText()
+        );
     }
 
     /**
-- 
GitLab