Skip to content

Commit ecda45e

Browse files
committed
Fix uncaught PDOException in manual lookup when sqlite is unavailable
1 parent f51cc46 commit ecda45e

2 files changed

Lines changed: 107 additions & 2 deletions

File tree

include/manual-lookup.inc

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,8 @@ function find_manual_page($lang, $keyword)
112112
if (in_array('sqlite', PDO::getAvailableDrivers(), true)) {
113113
if (file_exists(ProjectGlobals::getBackendRoot() . '/manual-lookup.sqlite')) {
114114
try {
115-
$dbh = new PDO( 'sqlite:' . ProjectGlobals::getBackendRoot() . '/manual-lookup.sqlite', '', '', [PDO::ATTR_PERSISTENT => true, PDO::ATTR_EMULATE_PREPARES => true] );
115+
// Check prepare()/execute() for false to fall back to the slow search
116+
$dbh = new PDO( 'sqlite:' . ProjectGlobals::getBackendRoot() . '/manual-lookup.sqlite', '', '', [PDO::ATTR_PERSISTENT => true, PDO::ATTR_EMULATE_PREPARES => true, PDO::ATTR_ERRMODE => PDO::ERRMODE_SILENT] );
116117
} catch (PDOException $e) {
117118
return find_manual_page_slow($lang, $keyword);
118119
}
@@ -211,7 +212,8 @@ function find_manual_page($lang, $keyword)
211212
}
212213
}
213214
} else {
214-
error_noservice();
215+
// prepare() failed, fall back to the slow search
216+
return find_manual_page_slow($langs[0], $kw);
215217
}
216218
}
217219

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace {
6+
// include/manual-lookup.inc defines global functions and depends on the global
7+
// get_manual_search_sections(). That lives in include/site.inc, which cannot be
8+
// required in isolation, so repeat the list from site.inc here.
9+
if (!function_exists('get_manual_search_sections')) {
10+
/** @return list<string> */
11+
function get_manual_search_sections(): array
12+
{
13+
return [
14+
"", "book.", "ref.", "function.", "class.", "enum.",
15+
"features.", "control-structures.", "language.",
16+
"about.", "faq.",
17+
];
18+
}
19+
}
20+
21+
require_once phpweb\ProjectGlobals::getProjectRoot() . '/include/manual-lookup.inc';
22+
}
23+
24+
namespace phpweb\Test\Unit\ManualLookup {
25+
26+
use phpweb\ProjectGlobals;
27+
use PHPUnit\Framework;
28+
29+
#[Framework\Attributes\CoversFunction('find_manual_page')]
30+
#[Framework\Attributes\CoversFunction('find_manual_page_slow')]
31+
#[Framework\Attributes\RunTestsInSeparateProcesses]
32+
#[Framework\Attributes\PreserveGlobalState(false)]
33+
final class FindManualPageTest extends Framework\TestCase
34+
{
35+
// Manual pages checked into public/manual/en/ that the searches below resolve to.
36+
private const SLOW_PATH_PAGE = '/manual/en/function.strpos.php';
37+
38+
private const FAST_PATH_PAGE = '/manual/en/function.rtrim.php';
39+
40+
private string $database;
41+
42+
protected function setUp(): void
43+
{
44+
$this->database = ProjectGlobals::getBackendRoot() . '/manual-lookup.sqlite';
45+
46+
// A database here means a live checkout with an rsynced manual, not a test one
47+
if (file_exists($this->database)) {
48+
self::markTestSkipped('manual-lookup.sqlite is present, refusing to overwrite it');
49+
}
50+
}
51+
52+
protected function tearDown(): void
53+
{
54+
@unlink($this->database);
55+
}
56+
57+
/**
58+
* Regression test for the production fatal:
59+
* Uncaught PDOException: SQLSTATE[HY000]: General error: 8
60+
* attempt to write a readonly database in include/manual-lookup.inc
61+
*
62+
* A read-only, locked or truncated database must fall back to the slow
63+
* search rather than throwing.
64+
*/
65+
public function testFallsBackToSlowSearchWhenSqliteQueryFails(): void
66+
{
67+
file_put_contents($this->database, 'this is not a sqlite database');
68+
69+
self::assertSame(self::SLOW_PATH_PAGE, find_manual_page('en', 'strpos'));
70+
}
71+
72+
public function testFallsBackToSlowSearchForDottedKeywordWhenSqliteQueryFails(): void
73+
{
74+
// A dotted keyword takes the other SQL branch, which must fall back too
75+
file_put_contents($this->database, 'this is not a sqlite database');
76+
77+
self::assertSame(self::SLOW_PATH_PAGE, find_manual_page('en', 'function.strpos'));
78+
}
79+
80+
public function testFallsBackToSlowSearchWhenNoDatabasePresent(): void
81+
{
82+
self::assertSame(self::SLOW_PATH_PAGE, find_manual_page('en', 'strpos'));
83+
}
84+
85+
/**
86+
* The fast path maps the keyword to a different page than the slow search
87+
* would find, so a match on it proves the database was really used.
88+
*/
89+
#[Framework\Attributes\RequiresPhpExtension('pdo_sqlite')]
90+
public function testUsesSqliteFastPathWhenDatabaseIsValid(): void
91+
{
92+
$dbh = new \PDO('sqlite:' . $this->database);
93+
$dbh->exec('CREATE TABLE fs (lang TEXT, prefix TEXT, keyword TEXT, name TEXT, prio INT)');
94+
$dbh->exec(sprintf(
95+
"INSERT INTO fs (lang, prefix, keyword, name, prio) VALUES ('en', 'function.', 'strpos', '%s', 3)",
96+
self::FAST_PATH_PAGE,
97+
));
98+
$dbh = null;
99+
100+
self::assertSame(self::FAST_PATH_PAGE, find_manual_page('en', 'strpos'));
101+
}
102+
}
103+
}

0 commit comments

Comments
 (0)