1<?php 2 3/* 4 5 This file is included directly on all manual pages, 6 and therefore is the entering point for all manual pages 7 to the website function collection. These functions display 8 the manual pages with headers and navigation. 9 10 The $PGI global variable is used to store all page related 11 information, including HTTP header related data. 12 13*/ 14 15// Ensure that our environment is set up 16include_once __DIR__ . '/prepend.inc'; 17 18// Set variable defaults 19$PGI = []; $SIDEBAR_DATA = ''; 20 21// ============================================================================= 22// User note display functions 23// ============================================================================= 24 25use phpweb\I18n\Languages; 26use phpweb\UserNotes\Sorter; 27use phpweb\UserNotes\UserNote; 28 29/** 30 * Print out all user notes for this manual page 31 * 32 * @param array<string, UserNote> $notes 33 */ 34function manual_notes($notes):void { 35 global $LANG; 36 37 // Get needed values 38 list($filename) = $GLOBALS['PGI']['this']; 39 40 // Drop file extension from the name 41 if (substr($filename, -4) == '.php') { 42 $filename = substr($filename, 0, -4); 43 } 44 45 $sorter = new Sorter(); 46 $sorter->sort($notes); 47 48 $repo = strtolower($LANG); 49 $addNote = autogen('add_a_note', $LANG); 50 // Link target to add a note to the current manual page, 51 // and it's extended form with a [+] image 52 $addnotelink = '/manual/add-note.php?sect=' . $filename . 53 '&repo=' . $repo . 54 '&redirect=' . $_SERVER['BASE_HREF']; 55 $addnotesnippet = make_link( 56 $addnotelink, 57 "+<small>$addNote</small>", 58 ); 59 60 $num_notes = count($notes); 61 $noteCountHtml = ''; 62 if ($num_notes) { 63 $noteCountHtml = "<span class=\"count\">$num_notes note" . ($num_notes == 1 ? '' : 's') . "</span>"; 64 } 65 66 $userContributedNotes = autogen('user_contributed_notes', $LANG); 67 echo <<<END_USERNOTE_HEADER 68<section id="usernotes"> 69 <div class="head"> 70 <span class="action">{$addnotesnippet}</span> 71 <h3 class="title">$userContributedNotes {$noteCountHtml}</h3> 72 </div> 73END_USERNOTE_HEADER; 74 75 // If we have no notes, then inform the user 76 if ($num_notes === 0) { 77 $noUserContributedNotes = autogen('no_user_notes', $LANG); 78 echo "\n <div class=\"note\">$noUserContributedNotes</div>"; 79 } else { 80 // If we have notes, print them out 81 echo '<div id="allnotes">'; 82 foreach ($notes as $note) { 83 manual_note_display($note); 84 } 85 echo "</div>\n"; 86 echo "<div class=\"foot\">$addnotesnippet</div>\n"; 87 } 88 echo "</section>"; 89} 90 91/** 92 * Get user notes from the appropriate text dump 93 * 94 * @return array<string, UserNote> 95 */ 96function manual_notes_load(string $id): array 97{ 98 $hash = substr(md5($id), 0, 16); 99 $notes_file = $_SERVER['DOCUMENT_ROOT'] . "/backend/notes/" . 100 substr($hash, 0, 2) . "/$hash"; 101 102 // Open the note file for reading and get the data (12KB) 103 // ..if it exists 104 if (!file_exists($notes_file)) { 105 return []; 106 } 107 $notes = []; 108 if ($fp = @fopen($notes_file, "r")) { 109 while (!feof($fp)) { 110 $line = chop(fgets($fp, 12288)); 111 if ($line == "") { continue; } 112 @list($id, $sect, $rate, $ts, $user, $note, $up, $down) = explode("|", $line); 113 $notes[$id] = new UserNote($id, $sect, $rate, $ts, $user, base64_decode($note, true), (int) $up, (int) $down); 114 } 115 fclose($fp); 116 } 117 return $notes; 118} 119 120// Print out one user note entry 121function manual_note_display(UserNote $note, $voteOption = true): void 122{ 123 if ($note->user) { 124 $name = "\n <strong class=\"user\"><em>" . htmlspecialchars($note->user) . "</em></strong>"; 125 } else { 126 $name = "<strong class=\"user\"><em>Anonymous</em></strong>"; 127 } 128 $name = ($note->id ? "\n <a href=\"#{$note->id}\" class=\"name\">$name</a><a class=\"genanchor\" href=\"#{$note->id}\"> ¶</a>" : "\n $name"); 129 130 // New date style will be relative time 131 $date = new DateTime("@{$note->ts}"); 132 $datestr = relTime($date); 133 $fdatestr = $date->format("Y-m-d h:i"); 134 $text = clean_note($note->text); 135 136 // Calculate note rating by up/down votes 137 $vote = $note->upvotes - $note->downvotes; 138 $p = floor(($note->upvotes / (($note->upvotes + $note->downvotes) ?: 1)) * 100); 139 $rate = !$p && !($note->upvotes + $note->downvotes) ? "no votes..." : "$p% like this..."; 140 141 // Vote User Notes Div 142 if ($voteOption) { 143 list($redir_filename) = $GLOBALS['PGI']['this']; 144 if (substr($redir_filename, -4) == '.php') { 145 $redir_filename = substr($redir_filename, 0, -4); 146 } 147 $rredir_filename = urlencode($redir_filename); 148 $votediv = <<<VOTEDIV 149 <div class="votes"> 150 <div id="Vu{$note->id}"> 151 <a href="/manual/vote-note.php?id={$note->id}&page={$rredir_filename}&vote=up" title="Vote up!" class="usernotes-voteu">up</a> 152 </div> 153 <div id="Vd{$note->id}"> 154 <a href="/manual/vote-note.php?id={$note->id}&page={$rredir_filename}&vote=down" title="Vote down!" class="usernotes-voted">down</a> 155 </div> 156 <div class="tally" id="V{$note->id}" title="{$rate}"> 157 {$vote} 158 </div> 159 </div> 160VOTEDIV; 161 } else { 162 $votediv = null; 163 } 164 165 // If the viewer is logged in, show admin options 166 if (isset($_COOKIE['IS_DEV']) && $note->id) { 167 168 $admin = "\n <span class=\"admin\">\n " . 169 170 make_popup_link( 171 'https://main.php.net/manage/user-notes.php?action=edit+' . $note->id, 172 '<img src="/images/notes-edit@2x.png" height="12" width="12" alt="edit note">', 173 'admin', 174 'scrollbars=yes,width=650,height=400', 175 ) . "\n " . 176 177 make_popup_link( 178 'https://main.php.net/manage/user-notes.php?action=reject+' . $note->id, 179 '<img src="/images/notes-reject@2x.png" height="12" width="12" alt="reject note">', 180 'admin', 181 'scrollbars=no,width=300,height=200', 182 ) . "\n " . 183 184 make_popup_link( 185 'https://main.php.net/manage/user-notes.php?action=delete+' . $note->id, 186 '<img src="/images/notes-delete@2x.png" height="12" width="12" alt="delete note">', 187 'admin', 188 'scrollbars=no,width=300,height=200', 189 ) . "\n </span>"; 190 191 } else { 192 $admin = ''; 193 } 194 195 echo <<<USER_NOTE_TEXT 196 197 <div class="note" id="{$note->id}">{$votediv}{$name}{$admin}<div class="date" title="$fdatestr"><strong>{$datestr}</strong></div> 198 <div class="text" id="Hcom{$note->id}"> 199{$text} 200 </div> 201 </div> 202USER_NOTE_TEXT; 203 204} 205 206function manual_navigation_breadcrumbs(array $setup) { 207 $menu = []; 208 foreach (array_reverse($setup["parents"]) as $parent) { 209 $menu[] = [ 210 "title" => $parent[1], 211 "link" => $parent[0], 212 ]; 213 } 214 215 // The index manual page has no parent.. 216 if ($setup["up"][0]) { 217 $last_item = [ 218 "title" => $setup["up"][1], 219 "link" => $setup["up"][0], 220 ]; 221 $menu[] = $last_item; 222 } 223 return $menu; 224} 225 226function manual_navigation_related(array $setup) { 227 $siblings = []; 228 foreach ($setup['toc'] as $entry) { 229 $siblings[] = [ 230 "title" => manual_navigation_methodname($entry[1]), 231 "link" => $entry[0], 232 "current" => $setup["this"][0] == $entry[0], 233 ]; 234 } 235 236 // The index manual page has no parent.. 237 if ($setup["up"][0]) { 238 $last_item = [ 239 "title" => $setup["up"][1], 240 "link" => $setup["up"][0], 241 ]; 242 $siblings = [array_merge($last_item, ["children" => $siblings])]; 243 } 244 return $siblings; 245} 246 247function manual_navigation_deprecated(array $setup) { 248 $methods = []; 249 foreach ((array)$setup['toc_deprecated'] as $entry) { 250 $methods[] = [ 251 "title" => manual_navigation_methodname($entry[1]), 252 "link" => $entry[0], 253 "current" => $setup["this"][0] == $entry[0], 254 ]; 255 } 256 257 return $methods; 258} 259 260function manual_navigation_methodname($methodname) { 261 // We strip out any class prefix here, we only want method names 262 if (strpos($methodname, '::') !== false && strpos($methodname, ' ') === false) { 263 $tmp = explode('::', $methodname); 264 $methodname = $tmp[1]; 265 } 266 267 // Add zero-width spaces to allow line-breaks at various characters 268 return str_replace(['-', '_'], ['-​', '_​'], $methodname); 269} 270 271// Set up variables important for this page 272// including HTTP header information 273function manual_setup($setup): void { 274 global $PGI, $MYSITE, $USERNOTES; 275 276 //TODO: get rid of this hack to get the related items into manual_footer 277 global $__RELATED; 278 279 if (!isset($setup["toc_deprecated"])) { 280 $setup["toc_deprecated"] = []; 281 } 282 $PGI = $setup; 283 // Set base href for this manual page 284 $base = 'manual/' . (new Languages())->convert($setup['head'][1]) . "/"; 285 $_SERVER['BASE_PAGE'] = $base . $setup['this'][0]; 286 $_SERVER['BASE_HREF'] = $MYSITE . $_SERVER['BASE_PAGE']; 287 288 $timestamps = [ 289 filemtime($_SERVER["DOCUMENT_ROOT"] . "/" . $_SERVER["BASE_PAGE"]), 290 filemtime($_SERVER["DOCUMENT_ROOT"] . "/include/prepend.inc"), 291 filemtime($_SERVER["DOCUMENT_ROOT"] . "/styles/theme-base.css"), 292 ]; 293 294 // Load user note for this page 295 $filename = $PGI['this'][0]; 296 297 // Drop file extension from the name 298 if (substr($filename, -4) == '.php') { 299 $filename = substr($filename, 0, -4); 300 } 301 $USERNOTES = manual_notes_load($filename); 302 if ($USERNOTES) { 303 $note = current($USERNOTES); 304 $timestamps[] = $note->ts; 305 } 306 307 $lastmod = max($timestamps); 308 309 $breadcrumbs = manual_navigation_breadcrumbs($setup); 310 $__RELATED['toc'] = manual_navigation_related($setup); 311 $__RELATED['toc_deprecated'] = manual_navigation_deprecated($setup); 312 313 $config = [ 314 "current" => "docs", 315 "breadcrumbs" => $breadcrumbs, 316 "languages" => array_keys(Languages::ACTIVE_ONLINE_LANGUAGES), 317 "meta-navigation" => [ 318 "contents" => $base . $setup["home"][0], 319 "index" => $base . $setup["up"][0], 320 "prev" => $base . $setup["prev"][0], 321 "next" => $base . $setup["next"][0], 322 ], 323 "lang" => $setup["head"][1], 324 "thispage" => $setup["this"][0], 325 "prev" => $setup["prev"], 326 "next" => $setup["next"], 327 "cache" => $lastmod, 328 ]; 329 site_header($setup["this"][1] . " - Manual ", $config); 330 331 $languageChooser = manual_language_chooser($config['lang'], $config['thispage']); 332 333 echo <<<PAGE_TOOLS 334 <div class="page-tools"> 335 <div class="change-language"> 336 {$languageChooser} 337 </div> 338 </div> 339PAGE_TOOLS; 340} 341 342function manual_language_chooser($currentlang, $currentpage) { 343 global $LANG; 344 345 // Prepare the form with all the options 346 $othersel = ' selected="selected"'; 347 $out = []; 348 foreach (Languages::ACTIVE_ONLINE_LANGUAGES as $lang => $text) { 349 $selected = ''; 350 if ($lang == $currentlang) { 351 $selected = ' selected="selected"'; 352 $othersel = ''; 353 } 354 $out[] = "<option value='$lang/$currentpage'$selected>$text</option>"; 355 } 356 $out[] = "<option value='help-translate.php'{$othersel}>Other</option>"; 357 $format_options = implode("\n" . str_repeat(' ', 6), $out); 358 359 $changeLanguage = autogen('change_language', $LANG); 360 $r = <<<CHANGE_LANG 361 <form action="/manual/change.php" method="get" id="changelang" name="changelang"> 362 <fieldset> 363 <label for="changelang-langs">$changeLanguage:</label> 364 <select onchange="document.changelang.submit()" name="page" id="changelang-langs"> 365 {$format_options} 366 </select> 367 </fieldset> 368 </form> 369CHANGE_LANG; 370 return trim($r); 371} 372 373function manual_footer($setup): void { 374 global $USERNOTES, $__RELATED, $LANG; 375 376 $id = substr($setup['this'][0], 0, -4); 377 $repo = strtolower($setup["head"][1]); // pt_BR etc. 378 379 $edit_url = "https://github.com/php/doc-{$repo}"; 380 // If the documentation source information is available (generated using 381 // doc-base/configure.php and PhD) then try and make a source-specific URL. 382 if (isset($setup['source'])) { 383 $source_lang = $setup['source']['lang']; 384 if ($source_lang === $repo || $source_lang === 'base') { 385 $edit_url = "https://github.com/php/doc-{$source_lang}/blob/master/{$setup['source']['path']}"; 386 } 387 } 388 389 $lastUpdate = ''; 390 if (isset($setup["history"]['modified']) && $setup["history"]['modified'] !== "") { 391 $modifiedDateTime = date_create($setup["history"]['modified']); 392 if ($modifiedDateTime !== false) { 393 $lastUpdate .= "Last updated on " . date_format($modifiedDateTime,"M d, Y (H:i T)"); 394 $lastUpdate .= (isset($setup["history"]['contributors'][0]) ? " by " . $setup["history"]['contributors'][0] : "") . "."; 395 } 396 } 397 398 $contributors = ''; 399 if (isset($setup["history"]['contributors']) && count($setup["history"]['contributors']) > 0) { 400 $contributors = '<a href="?contributors">All contributors.</a>'; 401 } 402 403 $improveThisPage = autogen('improve_this_page', $LANG); 404 $howToImproveThisPage = autogen('how_to_improve_this_page', $LANG); 405 $contributionGuidlinesOnGithub = autogen('contribution_guidlines_on_github', $LANG); 406 $submitPullRequest = autogen('submit_a_pull_request', $LANG); 407 $reportBug = autogen('report_a_bug', $LANG); 408 echo <<<CONTRIBUTE 409 <div class="contribute"> 410 <h3 class="title">$improveThisPage</h3> 411 <div> 412 $lastUpdate $contributors 413 </div> 414 <div class="edit-bug"> 415 <a href="https://github.com/php/doc-base/blob/master/README.md" title="$contributionGuidlinesOnGithub" target="_blank" rel="noopener noreferrer">$howToImproveThisPage</a> 416 • 417 <a href="{$edit_url}">$submitPullRequest</a> 418 • 419 <a href="https://github.com/php/doc-{$repo}/issues/new?body=From%20manual%20page:%20https:%2F%2Fphp.net%2F$id%0A%0A---">$reportBug</a> 420 </div> 421 </div> 422CONTRIBUTE; 423 424 manual_notes($USERNOTES); 425 site_footer([ 426 'related_menu' => $__RELATED['toc'], 427 'related_menu_deprecated' => $__RELATED['toc_deprecated'], 428 ]); 429} 430 431// This function takes a DateTime object and returns a formated string of the time difference relative to now 432function relTime(DateTime $date) { 433 $current = new DateTime(); 434 $diff = $current->diff($date); 435 $units = ["year" => $diff->format("%y"), 436 "month" => $diff->format("%m"), 437 "day" => $diff->format("%d"), 438 "hour" => $diff->format("%h"), 439 "minute" => $diff->format("%i"), 440 "second" => $diff->format("%s"), 441 ]; 442 $out = "just now..."; 443 foreach ($units as $unit => $amount) { 444 if (empty($amount)) { 445 continue; 446 } 447 $out = $amount . " " . ($amount == 1 ? $unit : $unit . "s") . " ago"; 448 break; 449 } 450 return $out; 451} 452 453function contributors($setup) { 454 if (!isset($_GET["contributors"]) 455 || !isset($setup["history"]["contributors"]) 456 || count($setup["history"]["contributors"]) < 1) { 457 return; 458 } 459 460 $contributorList = "<li>" . implode("</li><li>", $setup["history"]["contributors"]) . "</li>"; 461 462 echo <<<CONTRIBUTORS 463<div class="book"> 464 <h1 class="title">Output Buffering Control</h1> 465 The following have authored commits that contributed to this page: 466 <ul> 467 $contributorList 468 </ul> 469</div> 470CONTRIBUTORS; 471 manual_footer($setup); 472 exit; 473} 474 475function autogen(string $text, string $lang) { 476 static $translations = []; 477 478 $lang = ($lang === "") ? "en" : $lang; 479 $lang = strtolower($lang); 480 if (isset($translations[$lang])) { 481 if (isset($translations[$lang][$text]) && $translations[$lang][$text] !== "") { 482 return $translations[$lang][$text]; 483 } 484 if ($lang !== "en") { 485 // fall back to English if text is not defined for the given language 486 return autogen($text, "en"); 487 } 488 // we didn't find the English text either 489 throw new \InvalidArgumentException("Cannot autogenerate text for '$text'"); 490 } 491 492 $translationFile = __DIR__ . \DIRECTORY_SEPARATOR . "ui_translation" . \DIRECTORY_SEPARATOR . $lang . ".ini"; 493 494 if (!\file_exists($translationFile)) { 495 if ($lang !== "en") { 496 // fall back to English if translation file is not found 497 return autogen($text, "en"); 498 } 499 // we didn't find the English file either 500 throw new \Exception("Cannot find translation files"); 501 } 502 503 $translations[$lang] = \parse_ini_file($translationFile); 504 505 return autogen($text, $lang); 506} 507