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