xref: /web-php/include/shared-manual.inc (revision cdf59074)
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                   '&amp;repo=' . $repo .
54                   '&amp;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}\"> &para;</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}&amp;page={$rredir_filename}&amp;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}&amp;page={$rredir_filename}&amp;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(['-', '_'], ['-&#8203;', '_&#8203;'], $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>
416417        <a href="{$edit_url}">$submitPullRequest</a>
418419        <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