[v3,1/2] Allow listing all comments from a user

Message ID 20180722155436.22293-1-johannes@kyriasis.com
State Superseded, archived
Headers show
Series [v3,1/2] Allow listing all comments from a user | expand

Commit Message

Johannes Löthberg July 22, 2018, 3:54 p.m. UTC
Signed-off-by: Johannes Löthberg <johannes@kyriasis.com>
---
Since v2:
	Refactor things a bit to use the same pkg_comments.php for both 
	per-package and per-user comment listings

 web/html/account.php               | 20 +++++-
 web/html/css/aurweb.css            | 42 +++++++++++++
 web/html/index.php                 |  2 +
 web/html/pkgbase.php               | 10 ++-
 web/lib/acctfuncs.inc.php          | 42 +++++++++++++
 web/lib/aur.inc.php                | 53 ++++++++++++++++
 web/lib/credentials.inc.php        |  2 +
 web/lib/pkgbasefuncs.inc.php       | 10 ++-
 web/lib/pkgfuncs.inc.php           |  4 ++
 web/template/account_details.php   |  3 +
 web/template/account_edit_form.php |  1 +
 web/template/pkg_comments.php      | 99 ++++++++++++++++++++++--------
 12 files changed, 258 insertions(+), 30 deletions(-)

Comments

Lukas Fleischer Aug. 5, 2018, 4:31 p.m. UTC | #1
On Sun, 22 Jul 2018 at 17:54:35, Johannes Löthberg wrote:
> Signed-off-by: Johannes Löthberg <johannes@kyriasis.com>
> ---
> Since v2:
>         Refactor things a bit to use the same pkg_comments.php for both 
>         per-package and per-user comment listings
> 
>  web/html/account.php               | 20 +++++-
>  web/html/css/aurweb.css            | 42 +++++++++++++
>  web/html/index.php                 |  2 +
>  web/html/pkgbase.php               | 10 ++-
>  web/lib/acctfuncs.inc.php          | 42 +++++++++++++
>  web/lib/aur.inc.php                | 53 ++++++++++++++++
>  web/lib/credentials.inc.php        |  2 +
>  web/lib/pkgbasefuncs.inc.php       | 10 ++-
>  web/lib/pkgfuncs.inc.php           |  4 ++
>  web/template/account_details.php   |  3 +
>  web/template/account_edit_form.php |  1 +
>  web/template/pkg_comments.php      | 99 ++++++++++++++++++++++--------
>  12 files changed, 258 insertions(+), 30 deletions(-)
> 

Sorry for reviewing this so late. The patch is pretty invasive, though,
and we should make sure we are not missing anything.

> @@ -166,6 +166,24 @@ if (isset($_COOKIE["AURSID"])) {
>                                 $row["Username"]);
>                 }
>  
> +       } elseif ($action == "ListComments") {
> +               if (has_credential(CRED_ACCOUNT_LIST_COMMENTS)) {
> +                       # display the comment list if they're a TU/dev
> +
> +                       $total_comment_count = account_comments_count($row["ID"]);
> +                       [$pagination_templs, $per_page, $offset] = calculate_pagination($total_comment_count);

Interesting. I don't think we are using this syntax anywhere so far and
it might be a good idea to use list() instead, even if it is just for
consistency. Thoughts?

> +
> +                       $username = $row["Username"];
> +                       $uid = $row["ID"];
> +                       $comments = account_comments($row["ID"], $per_page, $offset);

We can replace the first parameter with $uid here.

> diff --git a/web/html/css/aurweb.css b/web/html/css/aurweb.css
> index f5e1037..593c9ae 100644
> --- a/web/html/css/aurweb.css
> +++ b/web/html/css/aurweb.css
> @@ -148,3 +148,45 @@ label.confirmation,
>         color: red;
>         font-weight: bold;
>  }
> +
> +.package-comments {
> +       margin-top: 1.5em;
> +}
> +
> +.comments-header {
> +       display: flex;
> +       justify-content: space-between;
> +       align-items: flex-start;
> +}
> +
> +/* arrowed headings */
> +.comments-header h3 span.text {
> +       display: block;
> +       background: #1794D1;
> +       font-size: 15px;
> +       padding: 2px 10px;
> +       color: white;
> +}
> +
> +.comments-header .comments-header-nav {
> +       align-self: flex-end;
> +}
> +
> +.comment-header {
> +       clear: both;
> +       font-size: 1em;
> +       margin-top: 1.5em;
> +       border-bottom: 1px dotted #bbb;
> +}
> +
> +.comments div {
> +       margin-bottom: 1em;
> +}
> +
> +.comments div p {
> +       margin-bottom: 0.5em;
> +}
> +
> +.comments .more {
> +       font-weight: normal;
> +}

What is the rationale for adding those new styles? Can't we just display
comments the way they are currently displayed under the package details?

> diff --git a/web/lib/acctfuncs.inc.php b/web/lib/acctfuncs.inc.php
> index df57375..192d879 100644
> --- a/web/lib/acctfuncs.inc.php
> +++ b/web/lib/acctfuncs.inc.php
> @@ -1403,3 +1403,45 @@ function accept_terms($uid, $termrev) {
>                 $dbh->exec($q);
>         }
>  }
> +
> +function account_comments($uid, $limit, $offset=0) {
> +       $dbh = DB::connect();
> +       $q = "SELECT PackageComments.ID, Comments, UsersID, ";
> +       $q.= "PackageBaseId, CommentTS, DelTS, EditedTS, B.UserName AS EditUserName, ";
> +       $q.= "PinnedTS, ";
> +       $q.= "C.UserName as DelUserName, RenderedComment, ";
> +       $q.= "PB.ID as PackageBaseID, PB.Name as PackageBaseName ";
> +       $q.= "FROM PackageComments ";
> +       $q.= "LEFT JOIN PackageBases PB ON PackageComments.PackageBaseID = PB.ID ";
> +       $q.= "LEFT JOIN Users A ON PackageComments.UsersID = A.ID ";
> +       $q.= "LEFT JOIN Users B ON PackageComments.EditedUsersID = B.ID ";
> +       $q.= "LEFT JOIN Users C ON PackageComments.DelUsersID = C.ID ";
> +       $q.= "WHERE A.ID = " . $dbh->quote($uid) . " ";
> +       $q.= "ORDER BY CommentTS DESC";
> +
> +       if ($limit > 0) {
> +               $q.=" LIMIT " . $limit;

Please add intval() around $limit here. Even if we know that the current
callers always pass integer values, it does not hurt to enforce the
conversion here to prevent from potential security issues in the future.

> +       }
> +
> +       if ($offset > 0) {
> +               $q.=" OFFSET " . $offset;

Same here. Add intval($offset).

> +       }
> +
> +       $result = $dbh->query($q);
> +       if (!$result) {
> +               return null;
> +       }
> +
> +       return $result->fetchAll();
> +}
> +
> +function account_comments_count($uid) {
> +       $dbh = DB::connect();
> +       $q = "SELECT COUNT(*) ";
> +       $q.= "FROM PackageComments ";
> +       $q.= "LEFT JOIN Users A ON PackageComments.UsersID = A.ID ";
> +       $q.= "WHERE A.ID = " . $dbh->quote($uid);
> +
> +       $result = $dbh->query($q);
> +       return $result->fetch(PDO::FETCH_NUM)[0];

Is this equivalent to "return $result->fetchColumn();"?

> +}
> diff --git a/web/lib/aur.inc.php b/web/lib/aur.inc.php
> index feb4006..89da81a 100644
> --- a/web/lib/aur.inc.php
> +++ b/web/lib/aur.inc.php
> @@ -705,3 +705,56 @@ function aur_location() {
>         }
>         return $location;
>  }
> +
> +/**
> + * Calculate pagination templates
> + *
> + * @return array The array of pagination templates, per page, and offset values
> + */
> +function calculate_pagination($total_comment_count) {
> [...]

Nice! I wonder whether we can use this helper function elsewhere, such
as for paginating package search results?

> diff --git a/web/lib/pkgbasefuncs.inc.php b/web/lib/pkgbasefuncs.inc.php
> index 72c33b6..953a581 100644
> --- a/web/lib/pkgbasefuncs.inc.php
> +++ b/web/lib/pkgbasefuncs.inc.php
> [...]
> @@ -71,6 +71,9 @@ function pkgbase_comments($base_id, $limit, $include_deleted, $only_pinned=false
>         if ($limit > 0) {
>                 $q.=" LIMIT " . $limit;
>         }
> +       if ($offset > 0) {
> +               $q.=" OFFSET " . $offset;

Same as above, please add intval().

Everything else looks good so far. And there's FS#59512 [1] (which you
are probably already aware of) to be addressed in a re-roll. Thanks a
lot put working on this!

Regards,
Lukas

[1] https://bugs.archlinux.org/task/59512
Johannes Löthberg Aug. 5, 2018, 11:58 p.m. UTC | #2
Quoting Lukas Fleischer (2018-08-05 18:31:46)
> > +                       [$pagination_templs, $per_page, $offset] = calculate_pagination($total_comment_count);
> 
> Interesting. I don't think we are using this syntax anywhere so far and
> it might be a good idea to use list() instead, even if it is just for
> consistency. Thoughts?
> 

While I prefer the short syntax since it's both less noisy and more 
consistent with the short array creation syntax, it doesn't matter much, 
so I'll change it.


> > +
> > +                       $username = $row["Username"];
> > +                       $uid = $row["ID"];
> > +                       $comments = account_comments($row["ID"], $per_page, $offset);
> 
> We can replace the first parameter with $uid here.

Fixed.


> 
> > diff --git a/web/html/css/aurweb.css b/web/html/css/aurweb.css
> > index f5e1037..593c9ae 100644
> > --- a/web/html/css/aurweb.css
> > +++ b/web/html/css/aurweb.css
> > @@ -148,3 +148,45 @@ label.confirmation,
> >         color: red;
> >         font-weight: bold;
> >  }
> > +
> > +.package-comments {
> > +       margin-top: 1.5em;
> > +}
> > +
> > +.comments-header {
> > +       display: flex;
> > +       justify-content: space-between;
> > +       align-items: flex-start;
> > +}
> > +
> > +/* arrowed headings */
> > +.comments-header h3 span.text {
> > +       display: block;
> > +       background: #1794D1;
> > +       font-size: 15px;
> > +       padding: 2px 10px;
> > +       color: white;
> > +}
> > +
> > +.comments-header .comments-header-nav {
> > +       align-self: flex-end;
> > +}
> > +
> > +.comment-header {
> > +       clear: both;
> > +       font-size: 1em;
> > +       margin-top: 1.5em;
> > +       border-bottom: 1px dotted #bbb;
> > +}
> > +
> > +.comments div {
> > +       margin-bottom: 1em;
> > +}
> > +
> > +.comments div p {
> > +       margin-bottom: 0.5em;
> > +}
> > +
> > +.comments .more {
> > +       font-weight: normal;
> > +}
> 
> What is the rationale for adding those new styles? Can't we just display
> comments the way they are currently displayed under the package details?
> 

I had to add more divs to be able to style the pagination links in a
reasonable manner, which means that the archweb.css styles no longer
apply.


> > diff --git a/web/lib/acctfuncs.inc.php b/web/lib/acctfuncs.inc.php
> > index df57375..192d879 100644
> > --- a/web/lib/acctfuncs.inc.php
> > +++ b/web/lib/acctfuncs.inc.php
> > @@ -1403,3 +1403,45 @@ function accept_terms($uid, $termrev) {
> >                 $dbh->exec($q);
> >         }
> >  }
> > +
> > +function account_comments($uid, $limit, $offset=0) {
> > +       $dbh = DB::connect();
> > +       $q = "SELECT PackageComments.ID, Comments, UsersID, ";
> > +       $q.= "PackageBaseId, CommentTS, DelTS, EditedTS, B.UserName AS EditUserName, ";
> > +       $q.= "PinnedTS, ";
> > +       $q.= "C.UserName as DelUserName, RenderedComment, ";
> > +       $q.= "PB.ID as PackageBaseID, PB.Name as PackageBaseName ";
> > +       $q.= "FROM PackageComments ";
> > +       $q.= "LEFT JOIN PackageBases PB ON PackageComments.PackageBaseID = PB.ID ";
> > +       $q.= "LEFT JOIN Users A ON PackageComments.UsersID = A.ID ";
> > +       $q.= "LEFT JOIN Users B ON PackageComments.EditedUsersID = B.ID ";
> > +       $q.= "LEFT JOIN Users C ON PackageComments.DelUsersID = C.ID ";
> > +       $q.= "WHERE A.ID = " . $dbh->quote($uid) . " ";
> > +       $q.= "ORDER BY CommentTS DESC";
> > +
> > +       if ($limit > 0) {
> > +               $q.=" LIMIT " . $limit;
> 
> Please add intval() around $limit here. Even if we know that the current
> callers always pass integer values, it does not hurt to enforce the
> conversion here to prevent from potential security issues in the future.
> 
> > +       }
> > +
> > +       if ($offset > 0) {
> > +               $q.=" OFFSET " . $offset;
> 
> Same here. Add intval($offset).
> 

Both fixed.


> > +       }
> > +
> > +       $result = $dbh->query($q);
> > +       if (!$result) {
> > +               return null;
> > +       }
> > +
> > +       return $result->fetchAll();
> > +}
> > +
> > +function account_comments_count($uid) {
> > +       $dbh = DB::connect();
> > +       $q = "SELECT COUNT(*) ";
> > +       $q.= "FROM PackageComments ";
> > +       $q.= "LEFT JOIN Users A ON PackageComments.UsersID = A.ID ";
> > +       $q.= "WHERE A.ID = " . $dbh->quote($uid);
> > +
> > +       $result = $dbh->query($q);
> > +       return $result->fetch(PDO::FETCH_NUM)[0];
> 
> Is this equivalent to "return $result->fetchColumn();"?
> 

Yeah, fixed.


> > +}
> > diff --git a/web/lib/aur.inc.php b/web/lib/aur.inc.php
> > index feb4006..89da81a 100644
> > --- a/web/lib/aur.inc.php
> > +++ b/web/lib/aur.inc.php
> > @@ -705,3 +705,56 @@ function aur_location() {
> >         }
> >         return $location;
> >  }
> > +
> > +/**
> > + * Calculate pagination templates
> > + *
> > + * @return array The array of pagination templates, per page, and offset values
> > + */
> > +function calculate_pagination($total_comment_count) {
> > [...]
> 
> Nice! I wonder whether we can use this helper function elsewhere, such
> as for paginating package search results?
> 

Yep, that's the plan.


> > diff --git a/web/lib/pkgbasefuncs.inc.php b/web/lib/pkgbasefuncs.inc.php
> > index 72c33b6..953a581 100644
> > --- a/web/lib/pkgbasefuncs.inc.php
> > +++ b/web/lib/pkgbasefuncs.inc.php
> > [...]
> > @@ -71,6 +71,9 @@ function pkgbase_comments($base_id, $limit, $include_deleted, $only_pinned=false
> >         if ($limit > 0) {
> >                 $q.=" LIMIT " . $limit;
> >         }
> > +       if ($offset > 0) {
> > +               $q.=" OFFSET " . $offset;
> 
> Same as above, please add intval().
> 

Fixed.


> Everything else looks good so far. And there's FS#59512 [1] (which you
> are probably already aware of) to be addressed in a re-roll. Thanks a
> lot put working on this!
> 

Yeah, I hadn't even thought about letting regular users list their own
comments, but there's no real reason not to, so fixed.

Patch

diff --git a/web/html/account.php b/web/html/account.php
index c30a89a..885952b 100644
--- a/web/html/account.php
+++ b/web/html/account.php
@@ -8,7 +8,7 @@  include_once('acctfuncs.inc.php');   # access Account specific functions
 $action = in_request("Action");
 
 $need_userinfo = array(
-	"DisplayAccount", "DeleteAccount", "AccountInfo", "UpdateAccount"
+	"DisplayAccount", "DeleteAccount", "AccountInfo", "UpdateAccount", "ListComments"
 );
 
 if (in_array($action, $need_userinfo)) {
@@ -166,6 +166,24 @@  if (isset($_COOKIE["AURSID"])) {
 				$row["Username"]);
 		}
 
+	} elseif ($action == "ListComments") {
+		if (has_credential(CRED_ACCOUNT_LIST_COMMENTS)) {
+			# display the comment list if they're a TU/dev
+
+			$total_comment_count = account_comments_count($row["ID"]);
+			[$pagination_templs, $per_page, $offset] = calculate_pagination($total_comment_count);
+
+			$username = $row["Username"];
+			$uid = $row["ID"];
+			$comments = account_comments($row["ID"], $per_page, $offset);
+
+			$comment_section = "account";
+			include('pkg_comments.php');
+
+		} else {
+			print __("You are not allowed to access this area.");
+		}
+
 	} else {
 		if (has_credential(CRED_ACCOUNT_SEARCH)) {
 			# display the search page if they're a TU/dev
diff --git a/web/html/css/aurweb.css b/web/html/css/aurweb.css
index f5e1037..593c9ae 100644
--- a/web/html/css/aurweb.css
+++ b/web/html/css/aurweb.css
@@ -148,3 +148,45 @@  label.confirmation,
 	color: red;
 	font-weight: bold;
 }
+
+.package-comments {
+	margin-top: 1.5em;
+}
+
+.comments-header {
+	display: flex;
+	justify-content: space-between;
+	align-items: flex-start;
+}
+
+/* arrowed headings */
+.comments-header h3 span.text {
+	display: block;
+	background: #1794D1;
+	font-size: 15px;
+	padding: 2px 10px;
+	color: white;
+}
+
+.comments-header .comments-header-nav {
+	align-self: flex-end;
+}
+
+.comment-header {
+	clear: both;
+	font-size: 1em;
+	margin-top: 1.5em;
+	border-bottom: 1px dotted #bbb;
+}
+
+.comments div {
+	margin-bottom: 1em;
+}
+
+.comments div p {
+	margin-bottom: 0.5em;
+}
+
+.comments .more {
+	font-weight: normal;
+}
diff --git a/web/html/index.php b/web/html/index.php
index 2c53cdd..b2cd840 100644
--- a/web/html/index.php
+++ b/web/html/index.php
@@ -142,6 +142,8 @@  if (!empty($tokens[1]) && '/' . $tokens[1] == get_pkg_route()) {
 				$_REQUEST['Action'] = "UpdateAccount";
 			} elseif ($tokens[3] == 'delete') {
 				$_REQUEST['Action'] = "DeleteAccount";
+			} elseif ($tokens[3] == 'comments') {
+				$_REQUEST['Action'] = "ListComments";
 			} else {
 				header("HTTP/1.0 404 Not Found");
 				include "./404.php";
diff --git a/web/html/pkgbase.php b/web/html/pkgbase.php
index cf9a6c6..46ad77e 100644
--- a/web/html/pkgbase.php
+++ b/web/html/pkgbase.php
@@ -43,6 +43,7 @@  if (isset($_POST['IDs'])) {
 
 /* Perform package base actions. */
 $via = isset($_POST['via']) ? $_POST['via'] : NULL;
+$return_to = isset($_POST['return_to']) ? $_POST['return_to'] : NULL;
 $ret = false;
 $output = "";
 $fragment = "";
@@ -133,7 +134,14 @@  if (check_token()) {
 			/* Redirect back to package request page on success. */
 			header('Location: ' . get_pkgreq_route());
 			exit();
-		} if (isset($base_id)) {
+		} elseif ((current_action("do_DeleteComment") ||
+		           current_action("do_UndeleteComment")) && $return_to) {
+			header('Location: ' . $return_to);
+			exit();
+		} elseif (current_action("do_PinComment") && $return_to) {
+			header('Location: ' . $return_to);
+			exit();
+		} elseif (isset($base_id)) {
 			/* Redirect back to package base page on success. */
 			header('Location: ' . get_pkgbase_uri($pkgbase_name) . $fragment);
 			exit();
diff --git a/web/lib/acctfuncs.inc.php b/web/lib/acctfuncs.inc.php
index df57375..192d879 100644
--- a/web/lib/acctfuncs.inc.php
+++ b/web/lib/acctfuncs.inc.php
@@ -1403,3 +1403,45 @@  function accept_terms($uid, $termrev) {
 		$dbh->exec($q);
 	}
 }
+
+function account_comments($uid, $limit, $offset=0) {
+	$dbh = DB::connect();
+	$q = "SELECT PackageComments.ID, Comments, UsersID, ";
+	$q.= "PackageBaseId, CommentTS, DelTS, EditedTS, B.UserName AS EditUserName, ";
+	$q.= "PinnedTS, ";
+	$q.= "C.UserName as DelUserName, RenderedComment, ";
+	$q.= "PB.ID as PackageBaseID, PB.Name as PackageBaseName ";
+	$q.= "FROM PackageComments ";
+	$q.= "LEFT JOIN PackageBases PB ON PackageComments.PackageBaseID = PB.ID ";
+	$q.= "LEFT JOIN Users A ON PackageComments.UsersID = A.ID ";
+	$q.= "LEFT JOIN Users B ON PackageComments.EditedUsersID = B.ID ";
+	$q.= "LEFT JOIN Users C ON PackageComments.DelUsersID = C.ID ";
+	$q.= "WHERE A.ID = " . $dbh->quote($uid) . " ";
+	$q.= "ORDER BY CommentTS DESC";
+
+	if ($limit > 0) {
+		$q.=" LIMIT " . $limit;
+	}
+
+	if ($offset > 0) {
+		$q.=" OFFSET " . $offset;
+	}
+
+	$result = $dbh->query($q);
+	if (!$result) {
+		return null;
+	}
+
+	return $result->fetchAll();
+}
+
+function account_comments_count($uid) {
+	$dbh = DB::connect();
+	$q = "SELECT COUNT(*) ";
+	$q.= "FROM PackageComments ";
+	$q.= "LEFT JOIN Users A ON PackageComments.UsersID = A.ID ";
+	$q.= "WHERE A.ID = " . $dbh->quote($uid);
+
+	$result = $dbh->query($q);
+	return $result->fetch(PDO::FETCH_NUM)[0];
+}
diff --git a/web/lib/aur.inc.php b/web/lib/aur.inc.php
index feb4006..89da81a 100644
--- a/web/lib/aur.inc.php
+++ b/web/lib/aur.inc.php
@@ -705,3 +705,56 @@  function aur_location() {
 	}
 	return $location;
 }
+
+/**
+ * Calculate pagination templates
+ *
+ * @return array The array of pagination templates, per page, and offset values
+ */
+function calculate_pagination($total_comment_count) {
+	/* Sanitize paging variables. */
+	if (isset($_GET["O"])) {
+		$_GET["O"] = max(intval($_GET["O"]), 0);
+	} else {
+		$_GET["O"] = 0;
+	}
+	$offset = $_GET["O"];
+
+	if (isset($_GET["PP"])) {
+		$_GET["PP"] = bound(intval($_GET["PP"]), 1, 250);
+	} else {
+		$_GET["PP"] = 10;
+	}
+	$per_page = $_GET["PP"];
+
+	// Page offsets start at zero, so page 2 has offset 1, which means that we
+	// need to add 1 to the offset to get the current page.
+	$current_page = ceil($offset / $per_page) + 1;
+	$num_pages = ceil($total_comment_count / $per_page);
+	$pagination_templs = array();
+
+	if ($current_page > 1) {
+		$previous_page = $current_page - 1;
+		$previous_offset = ($previous_page - 1) * $per_page;
+		$pagination_templs['&laquo; ' . __('First')] = 0;
+		$pagination_templs['&lsaquo; ' . __('Previous')] = $previous_offset;
+	}
+
+	if ($current_page - 5 > 1) {
+		$pagination_templs["..."] = false;
+	}
+
+	for ($i = max($current_page - 5, 1); $i <= min($num_pages, $current_page + 5); $i++) {
+		$pagination_templs[$i] = ($i - 1) * $per_page;
+	}
+
+	if ($current_page + 5 < $num_pages)
+		$pagination_templs["... "] = false;
+
+	if ($current_page < $num_pages) {
+		$pagination_templs[__('Next') . ' &rsaquo;'] = $current_page * $per_page;
+		$pagination_templs[__('Last') . ' &raquo;'] = ($num_pages - 1) * $per_page;
+	}
+
+	return [$pagination_templs, $per_page, $offset];
+}
diff --git a/web/lib/credentials.inc.php b/web/lib/credentials.inc.php
index d8698a8..5d90cfc 100644
--- a/web/lib/credentials.inc.php
+++ b/web/lib/credentials.inc.php
@@ -5,6 +5,7 @@  define("CRED_ACCOUNT_EDIT", 2);
 define("CRED_ACCOUNT_EDIT_DEV", 3);
 define("CRED_ACCOUNT_LAST_LOGIN", 4);
 define("CRED_ACCOUNT_SEARCH", 5);
+define("CRED_ACCOUNT_LIST_COMMENTS", 28);
 define("CRED_COMMENT_DELETE", 6);
 define("CRED_COMMENT_UNDELETE", 27);
 define("CRED_COMMENT_VIEW_DELETED", 22);
@@ -59,6 +60,7 @@  function has_credential($credential, $approved_users=array()) {
 	case CRED_ACCOUNT_EDIT:
 	case CRED_ACCOUNT_LAST_LOGIN:
 	case CRED_ACCOUNT_SEARCH:
+	case CRED_ACCOUNT_LIST_COMMENTS:
 	case CRED_COMMENT_DELETE:
 	case CRED_COMMENT_UNDELETE:
 	case CRED_COMMENT_VIEW_DELETED:
diff --git a/web/lib/pkgbasefuncs.inc.php b/web/lib/pkgbasefuncs.inc.php
index 72c33b6..953a581 100644
--- a/web/lib/pkgbasefuncs.inc.php
+++ b/web/lib/pkgbasefuncs.inc.php
@@ -44,7 +44,7 @@  function pkgbase_comments_count($base_id, $include_deleted, $only_pinned=false)
  *
  * @return array All package comment information for a specific package base
  */
-function pkgbase_comments($base_id, $limit, $include_deleted, $only_pinned=false) {
+function pkgbase_comments($base_id, $limit, $include_deleted, $only_pinned=false, $offset=0) {
 	$base_id = intval($base_id);
 	$limit = intval($limit);
 	if (!$base_id) {
@@ -71,6 +71,9 @@  function pkgbase_comments($base_id, $limit, $include_deleted, $only_pinned=false
 	if ($limit > 0) {
 		$q.=" LIMIT " . $limit;
 	}
+	if ($offset > 0) {
+		$q.=" OFFSET " . $offset;
+	}
 	$result = $dbh->query($q);
 	if (!$result) {
 		return null;
@@ -273,6 +276,7 @@  function pkgbase_display_details($base_id, $row, $SID="") {
 		include('pkgbase_details.php');
 
 		if ($SID) {
+			$comment_section = "package";
 			include('pkg_comment_box.php');
 		}
 
@@ -281,13 +285,17 @@  function pkgbase_display_details($base_id, $row, $SID="") {
 		$limit_pinned = isset($_GET['pinned']) ? 0 : 5;
 		$pinned = pkgbase_comments($base_id, $limit_pinned, false, true);
 		if (!empty($pinned)) {
+			$comment_section = "package";
 			include('pkg_comments.php');
 		}
 		unset($pinned);
 
+
 		$limit = isset($_GET['comments']) ? 0 : 10;
 		$comments = pkgbase_comments($base_id, $limit, $include_deleted);
+
 		if (!empty($comments)) {
+			$comment_section = "package";
 			include('pkg_comments.php');
 		}
 	}
diff --git a/web/lib/pkgfuncs.inc.php b/web/lib/pkgfuncs.inc.php
index ad25474..140b8fc 100644
--- a/web/lib/pkgfuncs.inc.php
+++ b/web/lib/pkgfuncs.inc.php
@@ -624,13 +624,17 @@  function pkg_display_details($id=0, $row, $SID="") {
 		$limit_pinned = isset($_GET['pinned']) ? 0 : 5;
 		$pinned = pkgbase_comments($base_id, $limit_pinned, false, true);
 		if (!empty($pinned)) {
+			$comment_section = "package";
 			include('pkg_comments.php');
 		}
 		unset($pinned);
 
+
 		$limit = isset($_GET['comments']) ? 0 : 10;
 		$comments = pkgbase_comments($base_id, $limit, $include_deleted);
+
 		if (!empty($comments)) {
+			$comment_section = "package";
 			include('pkg_comments.php');
 		}
 	}
diff --git a/web/template/account_details.php b/web/template/account_details.php
index 024bd9c..fa6b528 100644
--- a/web/template/account_details.php
+++ b/web/template/account_details.php
@@ -82,6 +82,9 @@ 
 					<?php if (can_edit_account($row)): ?>
 						<li><a href="<?= get_user_uri($row['Username']); ?>edit"><?= __("Edit this user's account") ?></a></li>
 					<?php endif; ?>
+					<?php if (has_credential(CRED_ACCOUNT_LIST_COMMENTS)): ?>
+						<li><a href="<?= get_user_uri($row['Username']); ?>comments"><?= __("List this user's comments") ?></a></li>
+					<?php endif; ?>
 					</ul></td>
 				</tr>
 			</table>
diff --git a/web/template/account_edit_form.php b/web/template/account_edit_form.php
index 6eff81b..38d5274 100644
--- a/web/template/account_edit_form.php
+++ b/web/template/account_edit_form.php
@@ -2,6 +2,7 @@ 
 <p>
 	<?= __('Click %shere%s if you want to permanently delete this account.', '<a href="' . get_user_uri($N) . 'delete/' . '">', '</a>') ?>
 	<?= __('Click %shere%s for user details.', '<a href="' . get_user_uri($N) . '">', '</a>') ?>
+	<?= __('Click %shere%s to list the comments made by this account.', '<a href="' . get_user_uri($N) . 'comments/' . '">', '</a>') ?>
 </p>
 
 <form id="edit-profile-form" action="<?= get_user_uri($N) . 'update/'; ?>" method="post">
diff --git a/web/template/pkg_comments.php b/web/template/pkg_comments.php
index 3e5e5cc..3001a34 100644
--- a/web/template/pkg_comments.php
+++ b/web/template/pkg_comments.php
@@ -1,28 +1,69 @@ 
 <?php
-if (!isset($count)) {
-	$count = pkgbase_comments_count($base_id, $include_deleted);
+if ($comment_section == "package") {
+	if (!isset($count)) {
+		$count = pkgbase_comments_count($base_id, $include_deleted);
+	}
 }
 ?>
-<div id="news">
-	<h3>
-		<?php if (!isset($comments)): ?>
-			<?php $comments = $pinned ?>
-			<a href="<?= htmlentities(get_pkgbase_uri($pkgbase_name), ENT_QUOTES) . '?' . mkurl('comments=all') ?>" title="<?= __('View all comments' , $count) ?> (<?= $count ?>)"><?= __('Pinned Comments') ?></a>
-			<span class="arrow"></span>
-		<?php else: ?>
-			<a href="<?= htmlentities(get_pkgbase_uri($pkgbase_name), ENT_QUOTES) . '?' . mkurl('comments=all') ?>" title="<?= __('View all comments' , $count) ?> (<?= $count ?>)"><?= __('Latest Comments') ?></a>
-			<span class="arrow"></span>
+
+
+<?php if ($comment_section == "package"): ?>
+<div class="comments package-comments">
+<?php else: ?>
+<div class="comments">
+<?php endif; ?>
+	<div class="comments-header">
+		<h3>
+			<?php if ($comment_section == "package"): ?>
+				<?php if (!isset($comments)): ?>
+					<?php $comments = $pinned ?>
+					<span class="text"><?= __('Pinned Comments') ?></span>
+					<span class="arrow"></span>
+				<?php else: ?>
+					<span class="text"><?= __('Latest Comments') ?></span>
+					<span class="arrow"></span>
+				<?php endif; ?>
+			<?php elseif ($comment_section == "account"): ?>
+				<?= __("Comments for") ?> <a href="<?= htmlentities(get_uri('/account/' . $username), ENT_QUOTES) ?>"><?= $username ?></a>
+			<?php endif; ?>
+		</h3>
+
+		<?php if (isset($pagination_templs) && count($pagination_templs) > 1): ?>
+			<p class="comments-header-nav">
+				<?php foreach ($pagination_templs as $pagenr => $pagestart): ?>
+					<?php if ($pagestart === false): ?>
+						<span class="page"><?= $pagenr ?></span>
+					<?php elseif ($pagestart === $offset): ?>
+						<span class="page"><?= $pagenr ?></span>
+					<?php else: ?>
+						<?php if ($comment_section == "package"): ?>
+							<a class="page" href="<?= htmlentities(get_pkgbase_uri($pkgbase_name), ENT_QUOTES) . '?' . mkurl('O=' . $pagestart) ?>"><?= $pagenr ?></a>
+						<?php else: ?>
+							<a class="page" href="<?= get_uri('/account/' . $username . '/comments/') . '?' . mkurl('O=' . $pagestart) ?>"><?= $pagenr ?></a>
+						<?php endif; ?>
+					<?php endif; ?>
+				<?php endforeach; ?>
+			</p>
 		<?php endif; ?>
-	</h3>
+	</div>
 
 	<?php foreach ($comments as $indx => $row): ?>
 		<?php
+		if ($comment_section == "account") {
+			$pkgbase_name = $row["PackageBaseName"];
+		}
+
 		$date_fmtd = date('Y-m-d H:i', $row['CommentTS']);
-		if ($row['UserName']) {
-			$user_fmtd = html_format_username($row['UserName']);
-			$heading = __('%s commented on %s', $user_fmtd, $date_fmtd);
-		} else {
-			$heading = __('Anonymous comment on %s', $date_fmtd);
+		if ($comment_section == "package") {
+			if ($row['UserName']) {
+				$user_fmtd = html_format_username($row['UserName']);
+				$heading = __('%s commented on %s', $user_fmtd, $date_fmtd);
+			} else {
+				$heading = __('Anonymous comment on %s', $date_fmtd);
+			}
+		} elseif ($comment_section == "account") {
+			$pkg_uri = '<a href=' . htmlspecialchars(get_pkg_uri($row['PackageBaseName']), ENT_QUOTES) . '>' . htmlspecialchars($row['PackageBaseName']) . '</a></td>';
+			$heading = __('Commented on package %s on %s', $pkg_uri, $date_fmtd);
 		}
 
 		$is_deleted = $row['DelTS'];
@@ -50,8 +91,13 @@  if (!isset($count)) {
 			}
 			$heading .= ')</span>';
 		}
+
+		$comment_classes = "comment-header";
+		if ($is_deleted) {
+			$comment_classes .= " comment-deleted";
+		}
 		?>
-		<h4 id="<?= isset($pinned) ? "pinned-" : "comment-" ?><?= $row['ID'] ?>"<?php if ($is_deleted): ?> class="comment-deleted"<?php endif; ?>>
+		<h4 id="<?= isset($pinned) ? "pinned-" : "comment-" ?><?= $row['ID'] ?>" class="<?= $comment_classes ?>">
 			<?= $heading ?>
 			<?php if ($is_deleted && has_credential(CRED_COMMENT_UNDELETE)): ?>
 				<form class="undelete-comment-form" method="post" action="<?= htmlspecialchars(get_pkgbase_uri($pkgbase_name), ENT_QUOTES); ?>">
@@ -59,6 +105,7 @@  if (!isset($count)) {
 						<input type="hidden" name="action" value="do_UndeleteComment" />
 						<input type="hidden" name="comment_id" value="<?= $row['ID'] ?>" />
 						<input type="hidden" name="token" value="<?= htmlspecialchars($_COOKIE['AURSID']) ?>" />
+						<input type="hidden" name="return_to" value="<?= htmlspecialchars($_SERVER["REQUEST_URI"], ENT_QUOTES) ?>" />
 						<input type="image" class="undelete-comment" src="/images/action-undo.min.svg" width="11" height="11" alt="<?= __('Undelete comment') ?>" title="<?= __('Undelete comment') ?>" name="submit" value="1" />
 					</fieldset>
 				</form>
@@ -70,6 +117,7 @@  if (!isset($count)) {
 						<input type="hidden" name="action" value="do_DeleteComment" />
 						<input type="hidden" name="comment_id" value="<?= $row['ID'] ?>" />
 						<input type="hidden" name="token" value="<?= htmlspecialchars($_COOKIE['AURSID']) ?>" />
+						<input type="hidden" name="return_to" value="<?= htmlspecialchars($_SERVER["REQUEST_URI"], ENT_QUOTES) ?>" />
 						<input type="image" class="delete-comment" src="/images/x.min.svg" width="11" height="11" alt="<?= __('Delete comment') ?>" title="<?= __('Delete comment') ?>" name="submit" value="1" />
 					</fieldset>
 				</form>
@@ -79,13 +127,14 @@  if (!isset($count)) {
 			<a href="<?= htmlspecialchars(get_pkgbase_uri($pkgbase_name) . 'edit-comment/?comment_id=' . $row['ID'], ENT_QUOTES) ?>" class="edit-comment" title="<?= __('Edit comment') ?>"><img src="/images/pencil.min.svg" alt="<?= __('Edit comment') ?>" width="11" height="11"></a>
 			<?php endif; ?>
 
-			<?php if (!$is_deleted && !$is_pinned && can_pin_comment_array($row) && !(pkgbase_comments_count($base_id, false, true) >= 5)): ?>
+			<?php if (!$is_deleted && !$is_pinned && can_pin_comment_array($row) && !(pkgbase_comments_count($row["PackageBaseID"], false, true) >= 5)): ?>
 				<form class="pin-comment-form" method="post" action="<?= htmlspecialchars(get_pkgbase_uri($pkgbase_name), ENT_QUOTES); ?>">
 					<fieldset style="display:inline;">
 						<input type="hidden" name="action" value="do_PinComment" />
 						<input type="hidden" name="comment_id" value="<?= $row['ID'] ?>" />
-						<input type="hidden" name="package_base" value="<?= $base_id ?>" />
+						<input type="hidden" name="package_base" value="<?= $row["PackageBaseID"] ?>" />
 						<input type="hidden" name="token" value="<?= htmlspecialchars($_COOKIE['AURSID']) ?>" />
+						<input type="hidden" name="return_to" value="<?= htmlspecialchars($_SERVER["REQUEST_URI"], ENT_QUOTES) ?>" />
 						<input type="image" class="pin-comment" src="/images/pin.min.svg" width="11" height="11" alt="<?= __('Pin comment') ?>" title="<?= __('Pin comment') ?>" name="submit" value="1" />
 					</fieldset>
 				</form>
@@ -97,6 +146,7 @@  if (!isset($count)) {
 						<input type="hidden" name="action" value="do_UnpinComment" />
 						<input type="hidden" name="comment_id" value="<?= $row['ID'] ?>" />
 						<input type="hidden" name="token" value="<?= htmlspecialchars($_COOKIE['AURSID']) ?>" />
+						<input type="hidden" name="return_to" value="<?= htmlspecialchars($_SERVER["REQUEST_URI"], ENT_QUOTES) ?>" />
 						<input type="image" class="pin-comment" src="/images/unpin.min.svg" width="11" height="11" alt="<?= __('Unpin comment') ?>" title="<?= __('Unpin comment') ?>" name="submit" value="1" />
 					</fieldset>
 				</form>
@@ -114,13 +164,8 @@  if (!isset($count)) {
 			</div>
 		</div>
 	<?php endforeach; ?>
-
-<?php if ($count > 10 && !isset($_GET['comments']) && !isset($pinned)): ?>
-	<h3>
-		<a href="<?= htmlentities(get_pkgbase_uri($pkgbase_name), ENT_QUOTES) . '?' . mkurl('comments=all') ?>" title="<?= __('View all comments') ?> (<?= $count ?>)"><?= __('All comments', $count) ?></a>
-	</h3>
-<?php endif; ?>
 </div>
+
 <script>
 $(document).ready(function() {
 	$('.edit-comment').click(function () {
@@ -133,7 +178,7 @@  $(document).ready(function() {
 		$.getJSON('<?= get_uri('/rpc') ?>', {
 			type: 'get-comment-form',
 			arg: comment_id,
-			base_id: <?= intval($base_id) ?>,
+			base_id: <?= intval($row["PackageBaseID"]) ?>,
 			pkgbase_name: <?= json_encode($pkgbase_name) ?>
 		}, function (data) {
 			remove_busy_indicator(_this);