Merge lp:~vasilev/psiphon/sprint3-579678 into lp:psiphon

Proposed by Robert Vasilev
Status: Merged
Approved by: Adam Kruger
Approved revision: 124
Merged at revision: 139
Proposed branch: lp:~vasilev/psiphon/sprint3-579678
Merge into: lp:psiphon
Diff against target: 338 lines (+215/-16)
4 files modified
trunk/www/config.php (+1/-0)
trunk/www/includes/lang.php (+65/-0)
trunk/www/user-edit.php (+22/-2)
trunk/www/users.php (+127/-14)
To merge this branch: bzr merge lp:~vasilev/psiphon/sprint3-579678
Reviewer Review Type Date Requested Status
Rod Pending
Review via email: mp+35380@code.launchpad.net

This proposal supersedes a proposal from 2010-09-04.

Description of the change

Paging, sorting and filtering have been implemented in users.php.

Upd:
Rod's comments on 2010-09-09 have been implemented.

To post a comment you must log in.
Revision history for this message
Rod (rod-psiphon) wrote : Posted in a previous version of this proposal

This was a team review. Here are our comments:

- After trying the new functionality, we have some minor changes that weren't in the original spec.

1. Display the total number of users in all cases, even when no filter.
2. Add the paging controls to the top of the page (but also leave at the bottom).
3. Originally when add/edit user and return to list, the new/modified user was shown in bold. Now it returns to the default with no filter and page 1. Can you make it return to the page with the new user, shown in bold? This isn't required: please make a bug to backlog it if it's too much work.

- For code readability, don't mix access control check and input validation ("if (!user_has_permission($config, $record_user, "accounts") || (isset($_GET["ord"]) && (abs($_GET["ord"]) > count($column_number_to_field_name))))"

Also, this check won't catch all cases. the logic is mixed in with the later if condition: "if (!$_GET["ord"] || !is_numeric($_GET["rod"]))". It would be easier to review and maintain if the input validation and normalization is in one place.

Should validate same value used, even if we can reason that it's safe and/or equivalent: e.g., abs((int)$_GET["ord"]) is used vs. abs($_GET["ord"]) is validated.

- Always escape user input in output, even if we don't think it's a risk: "$filterby_string = "&filterby=" . $_GET['filterby'];" and "$ord_string = "&ord=". $_GET["ord"];"

Also when making the page number links below:

if (isset($_GET["ord"])) {
    $ord_string = "&ord=". $_GET["ord"];
}

See other files for examples of escaping. We use the helper function escape_html.

- "SELECT COUNT(*) AS cnt FROM user WHERE {$query_where_sql}" is repeated twice in a row; could keep result in variable. Even though MySQL caches results. If you always display the user total (filter or no) then: 1. always get total; 2. get filtered count when filter.

- Please use a more descriptive variable name: "echo ($pn);"

review: Needs Fixing
Revision history for this message
Robert Vasilev (vasilev) wrote : Posted in a previous version of this proposal

Most of your recommendations have been implemented.

In detail:
> 1. Display the total number of users in all cases, even when no filter.
Done.

> 2. Add the paging controls to the top of the page (but also leave at the bottom).
Done.

> 3. Originally when add/edit user and return to list, the new/modified user was
> shown in bold. Now it returns to the default with no filter and page 1. Can
> you make it return to the page with the new user, shown in bold? This isn't
> required: please make a bug to backlog it if it's too much work.
Implemented for editing/disabling case.
For adding case it's quite unobvious to predict on which page new user will appear for different sorting modes.
It will be better to implement highlinghting feature for new users as anover ticket.

> - For code readability, don't mix access control check and input validation
> ...
> Should validate same value used, even if we can reason that it's safe and/or equivalent
Done.

> - Always escape user input in output, even if we don't think it's a risk:
...
> See other files for examples of escaping. We use the helper function escape_html.
Have not found any example of escaping integers with escape_html() in Psiphon code.
Escaping integers with escape_html() not implemented because of unnecessarity.

> - "SELECT COUNT(*) AS cnt FROM user WHERE {$query_where_sql}" is repeated
> twice in a row; could keep result in variable. Even though MySQL caches
> results. If you always display the user total (filter or no) then: 1. always
> get total; 2. get filtered count when filter.
Done. But could not 'keep result in variable' because of '$query_where_sql' may be modified by adding filtering conditions, and results will not be equivalent.

> - Please use a more descriptive variable name: "echo ($pn);"
Done.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'trunk/www/config.php'
2--- trunk/www/config.php 2010-06-28 20:44:20 +0000
3+++ trunk/www/config.php 2010-09-14 09:13:48 +0000
4@@ -58,6 +58,7 @@
5 $config[max_login_delay] = 30; // 30 seconds
6
7 $config['tickets_ppage'] = 20;
8+$config['users_ppage'] = 100;
9
10 $config["sender"] = "psiphonadmin@gmail.com";
11 $config["invitation_sender"] = "psiphonadmin@gmail.com";
12
13=== modified file 'trunk/www/includes/lang.php'
14--- trunk/www/includes/lang.php 2010-09-07 16:38:40 +0000
15+++ trunk/www/includes/lang.php 2010-09-14 09:13:48 +0000
16@@ -4630,6 +4630,71 @@
17 Please begin using this URL to log into Psiphon.",
18 ),
19
20+ 'user_class_filter' => array(
21+ 'en' => "User Class Filter",
22+ 'fa' => "User Class Filter",
23+ 'ru' => "User Class Filter",
24+ 'uz' => "User Class Filter",
25+ 'tk' => "User Class Filter",
26+ 'cn' => "User Class Filter",
27+ 'ar' => "User Class Filter",
28+ 'vt' => "User Class Filter",
29+ 'fr' => "User Class Filter",
30+ 'es' => "User Class Filter",
31+ ),
32+
33+ 'no_filter' => array(
34+ 'en' => "No filter",
35+ 'fa' => "No filter",
36+ 'ru' => "No filter",
37+ 'uz' => "No filter",
38+ 'tk' => "No filter",
39+ 'cn' => "No filter",
40+ 'ar' => "No filter",
41+ 'vt' => "No filter",
42+ 'fr' => "No filter",
43+ 'es' => "No filter",
44+ ),
45+
46+ 'apply_filter' => array(
47+ 'en' => "Apply filter",
48+ 'fa' => "Apply filter",
49+ 'ru' => "Apply filter",
50+ 'uz' => "Apply filter",
51+ 'tk' => "Apply filter",
52+ 'cn' => "Apply filter",
53+ 'ar' => "Apply filter",
54+ 'vt' => "Apply filter",
55+ 'fr' => "Apply filter",
56+ 'es' => "Apply filter",
57+ ),
58+
59+ 'filtered_items_count' => array(
60+ 'en' => "Filtered items count",
61+ 'fa' => "Filtered items count",
62+ 'ru' => "Filtered items count",
63+ 'uz' => "Filtered items count",
64+ 'tk' => "Filtered items count",
65+ 'cn' => "Filtered items count",
66+ 'ar' => "Filtered items count",
67+ 'vt' => "Filtered items count",
68+ 'fr' => "Filtered items count",
69+ 'es' => "Filtered items count",
70+ ),
71+
72+ 'total_users_count' => array(
73+ 'en' => "Total users",
74+ 'fa' => "Total users",
75+ 'ru' => "Total users",
76+ 'uz' => "Total users",
77+ 'tk' => "Total users",
78+ 'cn' => "Total users",
79+ 'ar' => "Total users",
80+ 'vt' => "Total users",
81+ 'fr' => "Total users",
82+ 'es' => "Total users",
83+ ),
84+
85 // DO NOT MODIFY THIS LINE -- END OF TRANSLATIONS
86 );
87
88
89=== modified file 'trunk/www/user-edit.php'
90--- trunk/www/user-edit.php 2010-07-16 19:17:57 +0000
91+++ trunk/www/user-edit.php 2010-09-14 09:13:48 +0000
92@@ -39,6 +39,25 @@
93 // 403.php exits
94 }
95
96+// Chaining paging, filtering, and sorting info back to user list page
97+$_GET['filterby'] = cast_str_to_int($_GET['filterby']);
98+$_GET['ofs'] = cast_str_to_int($_GET['ofs']);
99+$_GET['ord'] = cast_str_to_int($_GET['ord']);
100+$filterby_string = '';
101+if (isset($_GET['filterby'])) {
102+ $filterby_string = "&filterby=" . $_GET['filterby'];
103+}
104+$ord_string='';
105+if (isset($_GET["ord"])) {
106+ $ord_string = "&ord=". $_GET["ord"];
107+}
108+$ofs_string = '';
109+if (isset($_GET["ofs"])) {
110+ $ofs_string = "&ofs=". $_GET["ofs"];
111+}
112+$paging_sorting_filtering_params = $ofs_string.$ord_string.$filterby_string;
113+
114+
115 // This page has two modes: add and edit. If there are no GET params, then it's
116 // in add-mode. If there's an 'user_id' GET param, this it's in edit-mode. (The
117 // 'user_id' is the ID of the user to edit.)
118@@ -386,6 +405,7 @@
119
120 $action = getenv("SCRIPT_NAME");
121 if ($user_id !== Null) $action .= "?user_id=$user_id";
122+$action .= $paging_sorting_filtering_params;
123
124 include($_SERVER[DOCUMENT_ROOT]."/header.php");
125 ?>
126@@ -522,9 +542,9 @@
127 <input type="submit" value="<?=msg("submit")?>"/>
128 <br/>
129 <?if ($submit_success) {?>
130- <a href="users.php?user_id=<?=$user_id?>"><b><?=msg("back_to_users")?></b></a>
131+ <a href="users.php?user_id=<?=$user_id.$paging_sorting_filtering_params?>"><b><?=msg("back_to_users")?></b></a>
132 <?} else {?>
133- <a href="users.php"><?=msg("cancel")?></a>
134+ <a href="users.php?<?=$paging_sorting_filtering_params?>"><?=msg("cancel")?></a>
135 <?}?>
136
137 <?if ($page_mode == "ADD") {?>
138
139=== modified file 'trunk/www/users.php'
140--- trunk/www/users.php 2010-07-05 20:29:49 +0000
141+++ trunk/www/users.php 2010-09-14 09:13:48 +0000
142@@ -29,6 +29,8 @@
143
144 $user_edit_page = 'user-edit.php';
145
146+$column_number_to_field_name = array(1=>'uname', 2=>'email', 3=>'grp', 4=>'current_proxy', 5=>'proxies', 6=>'inv_actv', 7=>'inv_sent');
147+$default_sort_field = 1;
148 //
149 // Business logic
150 //
151@@ -40,13 +42,25 @@
152 include($_SERVER[DOCUMENT_ROOT]."/http-errors/403.php");
153 // 403.php exits
154 }
155-
156+
157 // Screen title. Must come before the header.php include.
158
159 $title = msg("users_management");
160
161 $_GET['disable'] = cast_str_to_int($_GET['disable']);
162 $last_modified_user_id = cast_str_to_int($_GET['user_id']);
163+$_GET['filterby'] = cast_str_to_int($_GET['filterby']);
164+$_GET['ord'] = cast_str_to_int($_GET['ord']);
165+// If 'ord' param is not set or invalid, sort by default column
166+if (!$_GET["ord"] || (abs($_GET["ord"]) > count($column_number_to_field_name)))
167+{
168+ $_GET["ord"] = (int)$default_sort_field;
169+}
170+$orderby_sql = " ORDER BY ".$column_number_to_field_name[abs($_GET["ord"])];
171+if ($_GET["ord"]<0)
172+{
173+ $orderby_sql.=" DESC";
174+}
175
176 // We need to build up the where clause that will be used in the main query.
177 $query_where_sql = get_user_managed_classes_sql_condition($config, $record_user);
178@@ -105,19 +119,118 @@
179 break;
180 }
181
182+// count total number of users (unfiltered)
183+$query = "SELECT COUNT(id) AS cnt FROM user WHERE {$query_where_sql}";
184+$result = db_query_execute($config, $query, convert_null_array($query_where_sql_params));
185+$record = db_fetch_result($config, $result);
186+$total_users_count = $record["cnt"];
187+db_free_result($result);
188+
189+$filterby_string = '';
190+if (isset($_GET['filterby'])) {
191+ $filterby_string = "&amp;filterby=" . $_GET['filterby'];
192+
193+ // apply user class filter
194+ $query_where_sql .= " AND user.grp = :filter_user_class";
195+ $query_where_sql_params[':filter_user_class'] = $_GET['filterby'];
196+ $selected_filter = $_GET['filterby'];
197+
198+ // count filtered users without LIMITs
199+ $query = "SELECT COUNT(id) AS cnt FROM user WHERE {$query_where_sql}";
200+ $result = db_query_execute($config, $query, convert_null_array($query_where_sql_params));
201+ $record = db_fetch_result($config, $result);
202+ $filtered_users_count = $record["cnt"];
203+ db_free_result($result);
204+}
205+
206+// pager (from tickets.php)/////////////////////////////////////////////////////
207+$query = "SELECT COUNT(*) AS cnt FROM user WHERE {$query_where_sql}";
208+$result = db_query_execute($config, $query, convert_null_array($query_where_sql_params));
209+$record = db_fetch_result($config, $result);
210+$count = $record["cnt"];
211+db_free_result($result);
212+
213+$per_page = $config['users_ppage'];
214+if (($_GET["ofs"]=(int)$_GET["ofs"])<0) $_GET["ofs"]=0;
215+$last_page=floor(abs($count-0.1)/$per_page);
216+if ($_GET["ofs"]>$last_page) include($_SERVER[DOCUMENT_ROOT]."/http-errors/404.php");
217+$ord_string='';
218+if (isset($_GET["ord"])) {
219+ $ord_string = "&amp;ord=". $_GET["ord"];
220+}
221+$ofs_string = '';
222+if (isset($_GET["ofs"])) {
223+ $ofs_string = "&amp;ofs=". $_GET["ofs"];
224+}
225+
226+$page_numbers_control='';
227+for ($page=0; $page<=$last_page; $page++)
228+{
229+ if ($page!=$_GET["ofs"])
230+ {
231+ $page_numbers_control.="<a href=\"".getenv("SCRIPT_NAME")."?ofs={$page}{$ord_string}{$filterby_string}\">".($page+1)."</a> &nbsp;\n";
232+ }
233+ else
234+ {
235+ $page_numbers_control.="[ <b>".($page+1)."</b> ] &nbsp;\n";
236+ }
237+}
238+
239+$limit_offset = abs((int)($_GET[ofs] * $per_page));
240+$limit_count = abs((int)$per_page);
241+
242+// end of pager ////////////////////////////////////////////////////////////////
243+$paging_sorting_filtering_params = $ofs_string.$ord_string.$filterby_string;
244+
245+function print_sort_header($col_number, $label, $filterby_string)
246+{
247+ echo "<a href=\"" . getenv("SCRIPT_NAME") . "?ord=";
248+ if ($_GET["ord"] == $col_number)
249+ echo "-{$col_number}{$filterby_string}\"><b>" . $label . " /\\</b>";
250+ elseif ($_GET["ord"] == -$col_number)
251+ echo "{$col_number}{$filterby_string}\"><b>" . $label . " \\/</b>";
252+ else
253+ echo "-{$col_number}{$filterby_string}\">" . $label;
254+ echo "</a>\n";
255+}
256+
257 include($_SERVER[DOCUMENT_ROOT]."/header.php");
258 ?>
259-
260+<div>
261+ <form method="GET" action="<?=getenv("SCRIPT_NAME")?>">
262+ <label><?=msg('user_class_filter') ?>: </label>
263+<?
264+
265+$user_classes = get_user_class_names($config, $record_user, 1, 1);
266+
267+echo_combo_from_array(
268+ 'filterby',
269+ $user_classes,
270+ Null, // no transform function
271+ $selected_filter,
272+ 1, msg('no_filter'));
273+
274+?>
275+<input type="submit" value="<?=msg("apply_filter") ?>">
276+<? if (isset($filtered_users_count)) {
277+ echo " &nbsp;&nbsp; ".msg('filtered_items_count').": ".$filtered_users_count." &nbsp;&nbsp; ";
278+}
279+echo msg('total_users_count').": ".$total_users_count;
280+?>
281+</form>
282+
283+</div>
284+<? echo $page_numbers_control; ?>
285 <table class="brd">
286 <tr>
287- <th><?=msg("name")?></th>
288- <th><?=msg("email")?></th>
289- <th><?=msg("grp")?></th>
290- <th><?=msg("current_proxy")?></th>
291- <th><?=msg("proxies")?></th>
292- <th><?=msg("invites_accepted")?></th>
293- <th><?=msg("invites_sent")?></th>
294- <th><a href="<?=$user_edit_page?>"><?=msg("add_new_user")?></a></th>
295+ <th><? print_sort_header(1, msg("name"), $filterby_string) ?></th>
296+ <th><? print_sort_header(2, msg("email"), $filterby_string)?></th>
297+ <th><? print_sort_header(3, msg("grp"), $filterby_string)?></th>
298+ <th><? print_sort_header(4, msg("current_proxy"), $filterby_string)?></th>
299+ <th><? print_sort_header(5, msg("proxies"), $filterby_string)?></th>
300+ <th><? print_sort_header(6, msg("invites_accepted"), $filterby_string)?></th>
301+ <th><? print_sort_header(7, msg("invites_sent"), $filterby_string)?></th>
302+ <th><a href="<?=$user_edit_page."?$paging_sorting_filtering_params" ?>"><?=msg("add_new_user")?></a></th>
303 </tr>
304
305 <?
306@@ -125,7 +238,7 @@
307 $query = "SELECT user.*, ".
308 "GROUP_CONCAT(userproxy.proxyid SEPARATOR '".$proxy_separator."') AS proxies ".
309 "FROM user LEFT OUTER JOIN userproxy ON user.id = userproxy.userid ".
310- "WHERE {$query_where_sql} GROUP BY uname ORDER BY uname";
311+ "WHERE {$query_where_sql} GROUP BY uname {$orderby_sql} LIMIT {$limit_offset}, {$limit_count}";
312
313 $result = db_query_execute($config, $query, convert_null_array($query_where_sql_params));
314
315@@ -168,12 +281,12 @@
316 echo "<td>{$_p}{$record['inv_sent']}{$_s}</td>\n";
317
318 echo "<td>{$_p}&nbsp;";
319- echo "<a href=\"".$user_edit_page."?user_id={$record[id]}\">".msg("edit")."</a> &nbsp; ";
320+ echo "<a href=\"".$user_edit_page."?user_id={$record[id]}{$paging_sorting_filtering_params}\">".msg("edit")."</a> &nbsp; ";
321 $t=msg($record[locked]?"enable_account":"disable_account");
322 if ($record[id]!=$record_user[id])
323 {
324 // The "fresh" GET parameter ensures that the disable link is always coloured as not-visited.
325- echo "<a href=\"".getenv("SCRIPT_NAME")."?disable={$record[id]}&amp;fresh=".bin2hex(microtime())."\" title=\"{$t}\">X</a>";
326+ echo "<a href=\"".getenv("SCRIPT_NAME")."?disable={$record[id]}{$paging_sorting_filtering_params}&amp;fresh=".bin2hex(microtime())."\" title=\"{$t}\">X</a>";
327 }
328 else
329 {
330@@ -188,7 +301,7 @@
331 ?>
332 </table>
333 <?
334-
335+echo "<br>\n".$page_numbers_control;
336 db_free_result($result);
337
338 include($_SERVER[DOCUMENT_ROOT]."/footer.php");

Subscribers

People subscribed via source and target branches