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