xref: /web-php/include/manual-lookup.inc (revision 45d49c7e)
1<?php
2
3// We need this for error reporting
4include_once __DIR__ . '/errors.inc';
5
6// Try to find some variations of keyword with $prefix in the $lang manual
7function tryprefix($lang, $keyword, $prefix)
8{
9    // Replace all underscores with hyphens (phpdoc convention)
10    $keyword = str_replace("_", "-", $keyword);
11
12    // Replace everything in parentheses with a hyphen (ie. function call)
13    $keyword = preg_replace("!\\(.*\\)!", "-", $keyword);
14
15    // Try the keyword with the prefix
16    $try = "/manual/{$lang}/{$prefix}{$keyword}.php";
17    if (@file_exists($_SERVER['DOCUMENT_ROOT'] . $try)) { return $try; }
18
19    // Drop out spaces, and try that keyword (if different)
20    $nosp = str_replace(" ", "", $keyword);
21    if ($nosp != $keyword) {
22        $try = "/manual/{$lang}/{$prefix}{$nosp}.php";
23        if (@file_exists($_SERVER['DOCUMENT_ROOT'] . $try)) { return $try; }
24    }
25
26    // Replace spaces with hyphens, and try that (if different)
27    $dasp = str_replace(" ", "-", $keyword);
28    if ($dasp != $keyword) {
29        $try = "/manual/{$lang}/{$prefix}{$dasp}.php";
30        if (@file_exists($_SERVER['DOCUMENT_ROOT'] . $try)) { return $try; }
31    }
32
33    // Remove hyphens (and underscores), and try that (if different)
34    $noul = str_replace("-", "", $keyword);
35    if ($noul != $keyword) {
36        $try = "/manual/{$lang}/{$prefix}{$noul}.php";
37        if (@file_exists($_SERVER['DOCUMENT_ROOT'] . $try)) { return $try; }
38    }
39
40    // urldecode() (%5C == \) Replace namespace sperators, and try that (if different)
41    $keyword = urldecode($keyword);
42    $noul = str_replace("\\", "-", $keyword);
43    if ($noul != $keyword) {
44        $try = "/manual/{$lang}/{$prefix}{$noul}.php";
45        if (@file_exists($_SERVER['DOCUMENT_ROOT'] . $try)) { return $try; }
46    }
47
48    // Replace first - with a dot and try that (for mysqli_ type entries)
49    // Only necessary when prefix is empty
50    if (empty($prefix)) {
51        $pos = strpos($keyword, '-');
52        if ($pos !== false) {
53            $keyword[$pos] = '.';
54
55            $try = "/manual/{$lang}/{$prefix}{$keyword}.php";
56            if (@file_exists($_SERVER['DOCUMENT_ROOT'] . $try)) { return $try; }
57        }
58    }
59
60    // Nothing found
61    return "";
62}
63
64// Try to find a manual page in a specified language
65// for the specified "keyword". Using the sqlite is
66// better because then stat() calls are eliminated.
67function find_manual_page_slow($lang, $keyword)
68{
69    // Possible prefixes to test
70    $sections = get_manual_search_sections();
71
72    // Remove .. for security reasons
73    $keyword = str_replace("..", "", $keyword);
74
75    // Try to find a manual page with the specified prefix
76    foreach ($sections as $section) {
77        $found = tryprefix($lang, $keyword, $section);
78        if ($found) { return $found; }
79    }
80
81    // Fall back to English, if the language was not English,
82    // and nothing was found so far for any of the prefixes
83    if ($lang != "en") {
84        foreach ($sections as $section) {
85            $found = tryprefix("en", $keyword, $section);
86            if ($found) { return $found; }
87        }
88    }
89
90    // BC: Few references pages where moved to book.
91    if (strpos($keyword, "ref.") === 0) {
92        $kw = substr_replace($keyword, "book.", 0, 4);
93        return find_manual_page($lang, $kw);
94    }
95
96    // Nothing found
97    return "";
98}
99
100// If sqlite is available on this mirror use that for manual
101// page shortcuts, so we avoid stat() calls on the server
102function find_manual_page($lang, $keyword)
103{
104    // If there is no sqlite support, or we are unable to
105    // open the database, fall back to normal search. Use
106    // open rather than popen to avoid any chance of confusion
107    // when rsync updates the database
108    $dbh = false;
109    if (class_exists('PDO')) {
110        if (in_array('sqlite', PDO::getAvailableDrivers(), true)) {
111            if (file_exists($_SERVER['DOCUMENT_ROOT'] . '/backend/manual-lookup.sqlite')) {
112                try {
113                    $dbh = new PDO( 'sqlite:' . $_SERVER['DOCUMENT_ROOT'] . '/backend/manual-lookup.sqlite', '', '', [PDO::ATTR_PERSISTENT => true, PDO::ATTR_EMULATE_PREPARES => true] );
114                } catch (PDOException $e) {
115                    return find_manual_page_slow($lang, $keyword);
116                }
117            }
118        }
119    }
120    if (!$dbh) {
121        return find_manual_page_slow($lang, $keyword);
122    }
123    $kw = $keyword;
124
125    // Try the preferred language first, then the
126    // English one in case no page is found
127    $langs = ($lang != 'en') ? [$lang, 'en'] : ['en'];
128
129    // Reformat keyword, drop anything in parenthesis --- except a search for the underscore only. (Bug #63490)
130    if ($keyword != '_') {
131        $keyword = str_replace('_', '-', $keyword);
132    }
133    if (strpos($keyword, '(') !== false) {
134        $keyword = preg_replace("!\\(.*\\)!", "-", $keyword);
135    }
136
137    // No keyword to search for
138    if (strlen($keyword) == 0) {
139        return "";
140    }
141
142    // If there is a dot in the $keyword, then a prefix
143    // is specfied, so we need to carry that on into the SQL
144    // search [eg. function.echo or function.mysql-close]
145
146    // Check for all the languages
147    foreach ($langs as $lang) {
148
149        // @todo consider alternative schemas for this data
150        // @todo utilize phd to generate this data, instead of the current systems/gen-phpweb-sqlite-db.php
151
152        /* Example data:
153           lang    = en
154           name    = /manual/en/function.str-replace.php
155           keyword = str-replace
156           prefix  = function.
157           prio    = 2
158
159           Therefore, the query below matches: str-replace, function.str-replace and function.str-replace.php
160           This also applies to other sections like book.foo, language.foo, example.foo, etc.
161           Note: $keyword replaces _ with - above, so _ variants also work
162        */
163        if (strpos($keyword, ".") > 0) {
164            $SQL = "SELECT name from fs WHERE lang = ? AND (name = ? OR keyword = ?) ORDER BY prio LIMIT 1";
165
166            $_keyword = $keyword;
167            if (pathinfo($keyword, PATHINFO_EXTENSION) !== 'php') {
168                $_keyword .= '.php';
169            }
170
171            $stm = $dbh->prepare($SQL);
172            if (!$stm) {
173                    return find_manual_page_slow($lang, $keyword);
174            }
175            $stm->execute([$lang, "/manual/{$lang}/{$_keyword}", $keyword]);
176
177        // Some partially specified URL is used
178        } else {
179
180            // List a few variations, plus a metaphone version
181            // FIXME: metaphone causes too many false positives, disable for now
182            //        the similar_text() search fallback works fine (see quickref.php)
183            //        if this change remains, adjust the gen-phpweb-sqlite script accordingly
184
185            $SQL = "SELECT name, prio FROM fs WHERE lang = :lang
186                    AND keyword IN (?, ?, ?, ?, ?) ORDER BY keyword = ? DESC, prio LIMIT 1";
187
188            $stm = $dbh->prepare($SQL);
189            if ($stm) {
190                $stm->execute([$lang, $keyword, str_replace('\\', '-', $keyword), str_replace(' ', '', $keyword), str_replace(' ', '-', $keyword), str_replace('-', '', $keyword), $keyword]);
191            }
192        }
193
194        // Successful query
195        if ($stm) {
196            $r = $stm->fetch(PDO::FETCH_NUM);
197
198            if (isset($r[0])) {
199                if (isset($r[1]) && $r[1] > 10 && strlen($keyword) < 4) {
200                    // "Match" found, but the keyword is so short
201                    // its probably bogus. Skip it
202                    continue;
203                }
204
205                // Match found
206                // But does the file really exist?
207                // @todo consider redirecting here, instead of including content within the 404
208                // @todo considering the file path is generated from the manual build, we can probably remove this file_exists() check
209                if (file_exists($_SERVER["DOCUMENT_ROOT"] . $r[0])) {
210                    return $r[0];
211                }
212            }
213        } else {
214            error_noservice();
215        }
216    }
217
218    // No match found
219    // @todo refactor. find_manual_page_slow() performs many of the same searches already performed above,
220    //       but uses file_exists() instead of sqlite. In other words, if sqlite was used, don't perform
221    //       all of the slow and unnecessary checks.
222    return find_manual_page_slow($langs[0], $kw);
223}
224