Author: Kamil Tekiela (kamil-tekiela) Committer: Derick Rethans (derickr) Date: 2026-01-13T14:00:36+01:00
Commit: https://github.com/php/web-php/commit/b31ec1c7f1ad8901a2a27df56e766a1bf236cea5 Raw diff: https://github.com/php/web-php/commit/b31ec1c7f1ad8901a2a27df56e766a1bf236cea5.diff Create UserNoteService class Changed paths: A src/UserNotes/UserNoteService.php M include/layout.inc M include/shared-manual.inc M manual/add-note.php M manual/vote-note.php Diff: diff --git a/include/layout.inc b/include/layout.inc index 02b6f733ca..98d8c845c1 100644 --- a/include/layout.inc +++ b/include/layout.inc @@ -164,21 +164,6 @@ function make_link(string $url, string $linktext = ''): string return sprintf("<a href=\"%s\">%s</a>", $url, $linktext ?: $url); } -// make_popup_link() -// return a hyperlink to something, within the site, that pops up a new window -// -function make_popup_link($url, $linktext = false, $target = false, $windowprops = "", $extras = false) { - return sprintf("<a href=\"%s\" target=\"%s\" onclick=\"window.open('%s','%s','%s');return false;\"%s>%s</a>", - htmlspecialchars($url, ENT_QUOTES | ENT_IGNORE), - ($target ?: "_new"), - htmlspecialchars($url, ENT_QUOTES | ENT_IGNORE), - ($target ?: "_new"), - $windowprops, - ($extras ? ' ' . $extras : ''), - ($linktext ?: $url), - ); -} - // Print a link for a downloadable file (including filesize) function download_link($file, $title): void { @@ -220,20 +205,6 @@ function clean($var) { return htmlspecialchars($var, ENT_QUOTES); } -// Clean out the content of one user note for printing to HTML -function clean_note($text) -{ - // Highlight PHP source - $text = highlight_php(trim($text), true); - - // Turn urls into links - return preg_replace( - '!((mailto:|(https?|ftp|nntp|news)://).*?)(\s|<|\)|"|\\\\|\'|$)!', - '<a href="\1" rel="nofollow" target="_blank">\1</a>\4', - $text, - ); -} - function display_errors($errors): void { echo '<div class="errors">'; diff --git a/include/shared-manual.inc b/include/shared-manual.inc index 5d10ea5df0..f14d63b7f1 100644 --- a/include/shared-manual.inc +++ b/include/shared-manual.inc @@ -23,185 +23,7 @@ $PGI = []; $SIDEBAR_DATA = ''; // ============================================================================= use phpweb\I18n\Languages; -use phpweb\UserNotes\Sorter; -use phpweb\UserNotes\UserNote; - -/** - * Print out all user notes for this manual page - * - * @param array<string, UserNote> $notes - */ -function manual_notes($notes):void { - global $LANG; - - // Get needed values - list($filename) = $GLOBALS['PGI']['this']; - - // Drop file extension from the name - if (substr($filename, -4) == '.php') { - $filename = substr($filename, 0, -4); - } - - $sorter = new Sorter(); - $sorter->sort($notes); - - $repo = strtolower($LANG); - $addNote = autogen('add_a_note', $LANG); - // Link target to add a note to the current manual page, - // and it's extended form with a [+] image - $addnotelink = '/manual/add-note.php?sect=' . $filename . - '&repo=' . $repo . - '&redirect=' . $_SERVER['BASE_HREF']; - $addnotesnippet = make_link( - $addnotelink, - "+<small>$addNote</small>", - ); - - $num_notes = count($notes); - $noteCountHtml = ''; - if ($num_notes) { - $noteCountHtml = "<span class=\"count\">$num_notes note" . ($num_notes == 1 ? '' : 's') . "</span>"; - } - - $userContributedNotes = autogen('user_contributed_notes', $LANG); - echo <<<END_USERNOTE_HEADER -<section id="usernotes"> - <div class="head"> - <span class="action">{$addnotesnippet}</span> - <h3 class="title">$userContributedNotes {$noteCountHtml}</h3> - </div> -END_USERNOTE_HEADER; - - // If we have no notes, then inform the user - if ($num_notes === 0) { - $noUserContributedNotes = autogen('no_user_notes', $LANG); - echo "\n <div class=\"note\">$noUserContributedNotes</div>"; - } else { - // If we have notes, print them out - echo '<div id="allnotes">'; - foreach ($notes as $note) { - manual_note_display($note); - } - echo "</div>\n"; - echo "<div class=\"foot\">$addnotesnippet</div>\n"; - } - echo "</section>"; -} - -/** - * Get user notes from the appropriate text dump - * - * @return array<string, UserNote> - */ -function manual_notes_load(string $id): array -{ - $hash = substr(md5($id), 0, 16); - $notes_file = $_SERVER['DOCUMENT_ROOT'] . "/backend/notes/" . - substr($hash, 0, 2) . "/$hash"; - - // Open the note file for reading and get the data (12KB) - // ..if it exists - if (!file_exists($notes_file)) { - return []; - } - $notes = []; - if ($fp = @fopen($notes_file, "r")) { - while (!feof($fp)) { - $line = chop(fgets($fp, 12288)); - if ($line == "") { continue; } - @list($id, $sect, $rate, $ts, $user, $note, $up, $down) = explode("|", $line); - $notes[$id] = new UserNote($id, $sect, $rate, $ts, $user, base64_decode($note, true), (int) $up, (int) $down); - } - fclose($fp); - } - return $notes; -} - -// Print out one user note entry -function manual_note_display(UserNote $note, $voteOption = true): void -{ - if ($note->user) { - $name = "\n <strong class=\"user\"><em>" . htmlspecialchars($note->user) . "</em></strong>"; - } else { - $name = "<strong class=\"user\"><em>Anonymous</em></strong>"; - } - $name = ($note->id ? "\n <a href=\"#{$note->id}\" class=\"name\">$name</a><a class=\"genanchor\" href=\"#{$note->id}\"> ¶</a>" : "\n $name"); - - // New date style will be relative time - $date = new DateTime("@{$note->ts}"); - $datestr = relTime($date); - $fdatestr = $date->format("Y-m-d h:i"); - $text = clean_note($note->text); - - // Calculate note rating by up/down votes - $vote = $note->upvotes - $note->downvotes; - $p = floor(($note->upvotes / (($note->upvotes + $note->downvotes) ?: 1)) * 100); - $rate = !$p && !($note->upvotes + $note->downvotes) ? "no votes..." : "$p% like this..."; - - // Vote User Notes Div - if ($voteOption) { - list($redir_filename) = $GLOBALS['PGI']['this']; - if (substr($redir_filename, -4) == '.php') { - $redir_filename = substr($redir_filename, 0, -4); - } - $rredir_filename = urlencode($redir_filename); - $votediv = <<<VOTEDIV - <div class="votes"> - <div id="Vu{$note->id}"> - <a href="/manual/vote-note.php?id={$note->id}&page={$rredir_filename}&vote=up" title="Vote up!" class="usernotes-voteu">up</a> - </div> - <div id="Vd{$note->id}"> - <a href="/manual/vote-note.php?id={$note->id}&page={$rredir_filename}&vote=down" title="Vote down!" class="usernotes-voted">down</a> - </div> - <div class="tally" id="V{$note->id}" title="{$rate}"> - {$vote} - </div> - </div> -VOTEDIV; - } else { - $votediv = null; - } - - // If the viewer is logged in, show admin options - if (isset($_COOKIE['IS_DEV']) && $note->id) { - - $admin = "\n <span class=\"admin\">\n " . - - make_popup_link( - 'https://main.php.net/manage/user-notes.php?action=edit+' . $note->id, - '<img src="/images/[email protected]" height="12" width="12" alt="edit note">', - 'admin', - 'scrollbars=yes,width=650,height=400', - ) . "\n " . - - make_popup_link( - 'https://main.php.net/manage/user-notes.php?action=reject+' . $note->id, - '<img src="/images/[email protected]" height="12" width="12" alt="reject note">', - 'admin', - 'scrollbars=no,width=300,height=200', - ) . "\n " . - - make_popup_link( - 'https://main.php.net/manage/user-notes.php?action=delete+' . $note->id, - '<img src="/images/[email protected]" height="12" width="12" alt="delete note">', - 'admin', - 'scrollbars=no,width=300,height=200', - ) . "\n </span>"; - - } else { - $admin = ''; - } - - echo <<<USER_NOTE_TEXT - - <div class="note" id="{$note->id}">{$votediv}{$name}{$admin}<div class="date" title="$fdatestr"><strong>{$datestr}</strong></div> - <div class="text" id="Hcom{$note->id}"> -{$text} - </div> - </div> -USER_NOTE_TEXT; - -} +use phpweb\UserNotes\UserNoteService; function manual_navigation_breadcrumbs(array $setup) { $menu = []; @@ -298,7 +120,9 @@ function manual_setup($setup): void { if (substr($filename, -4) == '.php') { $filename = substr($filename, 0, -4); } - $USERNOTES = manual_notes_load($filename); + + $userNoteService = new UserNoteService(); + $USERNOTES = $userNoteService->load($filename); if ($USERNOTES) { $note = current($USERNOTES); $timestamps[] = $note->ts; @@ -422,62 +246,18 @@ function manual_footer($setup): void { </div> CONTRIBUTE; - manual_notes($USERNOTES); + $userNoteService = new UserNoteService(); + $userNoteService->display($USERNOTES); site_footer([ 'related_menu' => $__RELATED['toc'], 'related_menu_deprecated' => $__RELATED['toc_deprecated'], ]); } -// This function takes a DateTime object and returns a formated string of the time difference relative to now -function relTime(DateTime $date) { - $current = new DateTime(); - $diff = $current->diff($date); - $units = ["year" => $diff->format("%y"), - "month" => $diff->format("%m"), - "day" => $diff->format("%d"), - "hour" => $diff->format("%h"), - "minute" => $diff->format("%i"), - "second" => $diff->format("%s"), - ]; - $out = "just now..."; - foreach ($units as $unit => $amount) { - if (empty($amount)) { - continue; - } - $out = $amount . " " . ($amount == 1 ? $unit : $unit . "s") . " ago"; - break; - } - return $out; -} - -function contributors($setup) { - if (!isset($_GET["contributors"]) - || !isset($setup["history"]["contributors"]) - || count($setup["history"]["contributors"]) < 1) { - return; - } - - $contributorList = "<li>" . implode("</li><li>", $setup["history"]["contributors"]) . "</li>"; - - echo <<<CONTRIBUTORS -<div class="book"> - <h1 class="title">Output Buffering Control</h1> - The following have authored commits that contributed to this page: - <ul> - $contributorList - </ul> -</div> -CONTRIBUTORS; - manual_footer($setup); - exit; -} - function autogen(string $text, string $lang) { static $translations = []; $lang = ($lang === "") ? "en" : $lang; - $lang = strtolower($lang); if (isset($translations[$lang])) { if (isset($translations[$lang][$text]) && $translations[$lang][$text] !== "") { return $translations[$lang][$text]; diff --git a/manual/add-note.php b/manual/add-note.php index 4cb6651366..522c4b509e 100644 --- a/manual/add-note.php +++ b/manual/add-note.php @@ -8,6 +8,7 @@ include __DIR__ . '/spam_challenge.php'; use phpweb\UserNotes\UserNote; +use phpweb\UserNotes\UserNoteService; site_header("Add Manual Note", ['css' => 'add-note.css']); @@ -146,9 +147,10 @@ if ($error) { echo "<p class=\"formerror\">$error</p>\n"; } // Print out preview of note + $userNoteService = new UserNoteService(); echo '<p>This is what your entry will look like, roughly:</p>'; echo '<div id="usernotes">'; - manual_note_display(new UserNote('', '', '', time(), $user, $note)); + $userNoteService->displaySingle(new UserNote('', '', '', time(), $user, $note)); echo '</div><br><br>'; } diff --git a/manual/vote-note.php b/manual/vote-note.php index d07689e8ed..94b4f69e80 100644 --- a/manual/vote-note.php +++ b/manual/vote-note.php @@ -1,4 +1,7 @@ <?php + +use phpweb\UserNotes\UserNoteService; + $_SERVER['BASE_PAGE'] = 'manual/vote-note.php'; include_once __DIR__ . '/../include/prepend.inc'; include_once __DIR__ . '/../include/posttohost.inc'; @@ -13,9 +16,10 @@ $BACKid = htmlspecialchars($_REQUEST['id'] ?? ''); $link = "/{$BACKpage}#{$BACKid}"; $master_url = "https://main.php.net/entry/user-notes-vote.php"; +$notes = new UserNoteService(); if ($_SERVER['REQUEST_METHOD'] == 'POST') { - if (isset($_SERVER['HTTP_X_JSON']) && $_SERVER['HTTP_X_JSON'] == 'On' && !empty($_REQUEST['id']) && !empty($_REQUEST['page']) && ($N = manual_notes_load($_REQUEST['page'])) && array_key_exists($_REQUEST['id'], $N) && !empty($_REQUEST['vote']) && ($_REQUEST['vote'] === 'up' || $_REQUEST['vote'] === 'down')) { + if (isset($_SERVER['HTTP_X_JSON']) && $_SERVER['HTTP_X_JSON'] == 'On' && !empty($_REQUEST['id']) && !empty($_REQUEST['page']) && ($N = $notes->load($_REQUEST['page'])) && array_key_exists($_REQUEST['id'], $N) && !empty($_REQUEST['vote']) && ($_REQUEST['vote'] === 'up' || $_REQUEST['vote'] === 'down')) { $response = []; $hash = substr(md5($_REQUEST['page']), 0, 16); $notes_file = $_SERVER['DOCUMENT_ROOT'] . "/backend/notes/" . substr($hash, 0, 2) . "/$hash"; @@ -51,7 +55,7 @@ echo json_encode($response); exit; } - if (!empty($_REQUEST['id']) && !empty($_REQUEST['page']) && ($N = manual_notes_load($_REQUEST['page'])) && array_key_exists($_REQUEST['id'], $N) && !empty($_REQUEST['vote']) && ($_REQUEST['vote'] === 'up' || $_REQUEST['vote'] === 'down')) { + if (!empty($_REQUEST['id']) && !empty($_REQUEST['page']) && ($N = $notes->load($_REQUEST['page'])) && array_key_exists($_REQUEST['id'], $N) && !empty($_REQUEST['vote']) && ($_REQUEST['vote'] === 'up' || $_REQUEST['vote'] === 'down')) { if (!empty($_POST['challenge']) && !empty($_POST['func']) || empty($_POST['arga']) || empty($_POST['argb'])) { if (!test_answer($_POST['func'], $_POST['arga'], $_POST['argb'], $_POST['challenge'])) { $error = "Incorrect answer! Please try again."; @@ -96,7 +100,7 @@ site_header("Vote On User Notes"); $headerset = true; - if (!empty($_REQUEST['id']) && !empty($_REQUEST['page']) && ($N = manual_notes_load($_REQUEST['page'])) && array_key_exists($_REQUEST['id'], $N) && !empty($_REQUEST['vote']) && ($_REQUEST['vote'] === 'up' || $_REQUEST['vote'] === 'down')) { + if (!empty($_REQUEST['id']) && !empty($_REQUEST['page']) && ($N = $notes->load($_REQUEST['page'])) && array_key_exists($_REQUEST['id'], $N) && !empty($_REQUEST['vote']) && ($_REQUEST['vote'] === 'up' || $_REQUEST['vote'] === 'down')) { ?> <div class="container" id="notes-dialog" style="width: 100%; padding-bottom: 15px; margin: auto;"> <div style="width: 100%; margin: auto;"><h1>Voting</h1></div> @@ -118,7 +122,7 @@ <?php $backID = htmlspecialchars($_REQUEST['id']); $backPAGE = htmlspecialchars($_REQUEST['page']); - manual_note_display($N[$_REQUEST['id']], false); + $notes->displaySingle($N[$_REQUEST['id']], false); ?> </div> <div style="width: 90%; margin: auto;"><p><a href="<?php echo "/{$backPAGE}#{$backID}"; ?>"><< Back to user notes page</a></p></div> @@ -171,7 +175,7 @@ <?php $backID = htmlspecialchars($_REQUEST['id']); $backPAGE = htmlspecialchars($_REQUEST['page']); - manual_note_display($N[$_REQUEST['id']], false); + $notes->displaySingle($N[$_REQUEST['id']], false); ?> </div> <div style="width: 90%; margin: auto;"><p><a href="<?php echo "/{$backPAGE}#{$backID}"; ?>"><< Back to user notes page</a></p></div> diff --git a/src/UserNotes/UserNoteService.php b/src/UserNotes/UserNoteService.php new file mode 100644 index 0000000000..5e49ad9b55 --- /dev/null +++ b/src/UserNotes/UserNoteService.php @@ -0,0 +1,237 @@ +<?php + +namespace phpweb\UserNotes; + +final class UserNoteService +{ + /** + * Get user notes from the appropriate text dump + * + * @return array<string, UserNote> + */ + public function load(string $id): array + { + $hash = substr(md5($id), 0, 16); + $notes_file = $_SERVER['DOCUMENT_ROOT'] . "/backend/notes/" . substr($hash, 0, 2) . "/$hash"; + + // Open the note file for reading and get the data (12KB) + // ..if it exists + if (!file_exists($notes_file)) { + return []; + } + $notes = []; + if ($fp = @fopen($notes_file, "r")) { + while (!feof($fp)) { + $line = chop(fgets($fp, 12288)); + if ($line == "") { continue; } + @list($id, $sect, $rate, $ts, $user, $note, $up, $down) = explode("|", $line); + $notes[$id] = new UserNote($id, $sect, $rate, $ts, $user, base64_decode($note, true), (int) $up, (int) $down); + } + fclose($fp); + } + return $notes; + } + + /** + * Print out all user notes for this manual page + * + * @param array<string, UserNote> $notes + */ + public function display($notes):void { + global $LANG; + + // Get needed values + list($filename) = $GLOBALS['PGI']['this']; + + // Drop file extension from the name + if (substr($filename, -4) == '.php') { + $filename = substr($filename, 0, -4); + } + + $sorter = new Sorter(); + $sorter->sort($notes); + + $addNote = autogen('add_a_note', $LANG); + $repo = strtolower($LANG); + // Link target to add a note to the current manual page, + // and it's extended form with a [+] image + $addnotelink = '/manual/add-note.php?sect=' . $filename . + '&repo=' . $repo . + '&redirect=' . $_SERVER['BASE_HREF']; + $addnotesnippet = make_link( + $addnotelink, + "+<small>$addNote</small>", + ); + + $num_notes = count($notes); + $noteCountHtml = ''; + if ($num_notes) { + $noteCountHtml = "<span class=\"count\">$num_notes note" . ($num_notes == 1 ? '' : 's') . "</span>"; + } + + $userContributedNotes = autogen('user_contributed_notes', $LANG); + echo <<<END_USERNOTE_HEADER +<section id="usernotes"> + <div class="head"> + <span class="action">{$addnotesnippet}</span> + <h3 class="title">$userContributedNotes {$noteCountHtml}</h3> + </div> +END_USERNOTE_HEADER; + + // If we have no notes, then inform the user + if ($num_notes === 0) { + $noUserContributedNotes = autogen('no_user_notes', $LANG); + echo "\n <div class=\"note\">$noUserContributedNotes</div>"; + } else { + // If we have notes, print them out + echo '<div id="allnotes">'; + foreach ($notes as $note) { + $this->displaySingle($note); + } + echo "</div>\n"; + echo "<div class=\"foot\">$addnotesnippet</div>\n"; + } + echo "</section>"; + } + + /** + * Print out one user note entry + */ + public function displaySingle(UserNote $note, $voteOption = true): void + { + if ($note->user) { + $name = "\n <strong class=\"user\"><em>" . htmlspecialchars($note->user) . "</em></strong>"; + } else { + $name = "<strong class=\"user\"><em>Anonymous</em></strong>"; + } + $name = ($note->id ? "\n <a href=\"#{$note->id}\" class=\"name\">$name</a><a class=\"genanchor\" href=\"#{$note->id}\"> ¶</a>" : "\n $name"); + + // New date style will be relative time + $date = new \DateTime("@{$note->ts}"); + $datestr = $this->relTime($date); + $fdatestr = $date->format("Y-m-d h:i"); + $text = $this->cleanContent($note->text); + + // Calculate note rating by up/down votes + $vote = $note->upvotes - $note->downvotes; + $p = floor(($note->upvotes / (($note->upvotes + $note->downvotes) ?: 1)) * 100); + $rate = !$p && !($note->upvotes + $note->downvotes) ? "no votes..." : "$p% like this..."; + + // Vote User Notes Div + if ($voteOption) { + list($redir_filename) = $GLOBALS['PGI']['this']; + if (substr($redir_filename, -4) == '.php') { + $redir_filename = substr($redir_filename, 0, -4); + } + $rredir_filename = urlencode($redir_filename); + $votediv = <<<VOTEDIV + <div class="votes"> + <div id="Vu{$note->id}"> + <a href="/manual/vote-note.php?id={$note->id}&page={$rredir_filename}&vote=up" title="Vote up!" class="usernotes-voteu">up</a> + </div> + <div id="Vd{$note->id}"> + <a href="/manual/vote-note.php?id={$note->id}&page={$rredir_filename}&vote=down" title="Vote down!" class="usernotes-voted">down</a> + </div> + <div class="tally" id="V{$note->id}" title="{$rate}"> + {$vote} + </div> + </div> +VOTEDIV; + } else { + $votediv = null; + } + + // If the viewer is logged in, show admin options + if (isset($_COOKIE['IS_DEV']) && $note->id) { + + $admin = "\n <span class=\"admin\">\n " . + + $this->makePopupLink( + 'https://main.php.net/manage/user-notes.php?action=edit+' . $note->id, + '<img src="/images/[email protected]" height="12" width="12" alt="edit note">', + 'admin', + 'scrollbars=yes,width=650,height=400', + ) . "\n " . + + $this->makePopupLink( + 'https://main.php.net/manage/user-notes.php?action=reject+' . $note->id, + '<img src="/images/[email protected]" height="12" width="12" alt="reject note">', + 'admin', + 'scrollbars=no,width=300,height=200', + ) . "\n " . + + $this->makePopupLink( + 'https://main.php.net/manage/user-notes.php?action=delete+' . $note->id, + '<img src="/images/[email protected]" height="12" width="12" alt="delete note">', + 'admin', + 'scrollbars=no,width=300,height=200', + ) . "\n </span>"; + + } else { + $admin = ''; + } + + echo <<<USER_NOTE_TEXT + + <div class="note" id="{$note->id}">{$votediv}{$name}{$admin}<div class="date" title="$fdatestr"><strong>{$datestr}</strong></div> + <div class="text" id="Hcom{$note->id}"> +{$text} + </div> + </div> +USER_NOTE_TEXT; + } + + // Clean out the content of one user note for printing to HTML + private function cleanContent(string $text): string + { + // Highlight PHP source + $text = highlight_php(trim($text), true); + + // Turn urls into links + return preg_replace( + '!((mailto:|(https?|ftp|nntp|news)://).*?)(\s|<|\)|"|\\\\|\'|$)!', + '<a href="\1" rel="nofollow" target="_blank">\1</a>\4', + $text, + ); + } + + /** + * This function takes a DateTime object and returns a formated string of the time difference relative to now + */ + private function relTime(\DateTime $date): string + { + $current = new \DateTime(); + $diff = $current->diff($date); + $units = ["year" => $diff->format("%y"), + "month" => $diff->format("%m"), + "day" => $diff->format("%d"), + "hour" => $diff->format("%h"), + "minute" => $diff->format("%i"), + "second" => $diff->format("%s"), + ]; + $out = "just now..."; + foreach ($units as $unit => $amount) { + if (empty($amount)) { + continue; + } + $out = $amount . " " . ($amount == 1 ? $unit : $unit . "s") . " ago"; + break; + } + return $out; + } + + /** + * Return a hyperlink to something, within the site, that pops up a new window + */ + private function makePopupLink(string $url, string $linktext = '', string $target = '', string $windowprops = ''): string + { + return sprintf("<a href=\"%s\" target=\"%s\" onclick=\"window.open('%s','%s','%s');return false;\"%s>%s</a>", + htmlspecialchars($url, ENT_QUOTES | ENT_IGNORE), + ($target ?: "_new"), + htmlspecialchars($url, ENT_QUOTES | ENT_IGNORE), + ($target ?: "_new"), + $windowprops, + ($linktext ?: $url), + ); + } +}
