userprefs.cgi 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572
  1. #!/usr/bin/env perl -wT
  2. # -*- Mode: perl; indent-tabs-mode: nil -*-
  3. #
  4. # The contents of this file are subject to the Mozilla Public
  5. # License Version 1.1 (the "License"); you may not use this file
  6. # except in compliance with the License. You may obtain a copy of
  7. # the License at http://www.mozilla.org/MPL/
  8. #
  9. # Software distributed under the License is distributed on an "AS
  10. # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
  11. # implied. See the License for the specific language governing
  12. # rights and limitations under the License.
  13. #
  14. # The Original Code is the Bugzilla Bug Tracking System.
  15. #
  16. # Contributor(s): Terry Weissman <terry@mozilla.org>
  17. # Dan Mosedale <dmose@mozilla.org>
  18. # Alan Raetz <al_raetz@yahoo.com>
  19. # David Miller <justdave@syndicomm.com>
  20. # Christopher Aillon <christopher@aillon.com>
  21. # Gervase Markham <gerv@gerv.net>
  22. # Vlad Dascalu <jocuri@softhome.net>
  23. # Shane H. W. Travis <travis@sedsystems.ca>
  24. use strict;
  25. use lib qw(. lib);
  26. use Bugzilla;
  27. use Bugzilla::Constants;
  28. use Bugzilla::Search;
  29. use Bugzilla::Util;
  30. use Bugzilla::Error;
  31. use Bugzilla::User;
  32. use Bugzilla::Token;
  33. my $template = Bugzilla->template;
  34. local our $vars = {};
  35. ###############################################################################
  36. # Each panel has two functions - panel Foo has a DoFoo, to get the data
  37. # necessary for displaying the panel, and a SaveFoo, to save the panel's
  38. # contents from the form data (if appropriate).
  39. # SaveFoo may be called before DoFoo.
  40. ###############################################################################
  41. sub DoAccount {
  42. my $dbh = Bugzilla->dbh;
  43. my $user = Bugzilla->user;
  44. ($vars->{'realname'}) = $dbh->selectrow_array(
  45. "SELECT realname FROM profiles WHERE userid = ?", undef, $user->id);
  46. if(Bugzilla->params->{'allowemailchange'}
  47. && Bugzilla->user->authorizer->can_change_email) {
  48. # First delete old tokens.
  49. Bugzilla::Token::CleanTokenTable();
  50. my @token = $dbh->selectrow_array(
  51. "SELECT tokentype, issuedate + " .
  52. $dbh->sql_interval(MAX_TOKEN_AGE, 'DAY') . ", eventdata
  53. FROM tokens
  54. WHERE userid = ?
  55. AND tokentype LIKE 'email%'
  56. ORDER BY tokentype ASC " . $dbh->sql_limit(1), undef, $user->id);
  57. if (scalar(@token) > 0) {
  58. my ($tokentype, $change_date, $eventdata) = @token;
  59. $vars->{'login_change_date'} = $change_date;
  60. if($tokentype eq 'emailnew') {
  61. my ($oldemail,$newemail) = split(/:/,$eventdata);
  62. $vars->{'new_login_name'} = $newemail;
  63. }
  64. }
  65. }
  66. }
  67. sub SaveAccount {
  68. my $cgi = Bugzilla->cgi;
  69. my $dbh = Bugzilla->dbh;
  70. my $user = Bugzilla->user;
  71. my $pwd1 = $cgi->param('new_password1');
  72. my $pwd2 = $cgi->param('new_password2');
  73. if ($user->authorizer->can_change_password
  74. && ($cgi->param('Bugzilla_password') ne "" || $pwd1 ne "" || $pwd2 ne ""))
  75. {
  76. my ($oldcryptedpwd) = $dbh->selectrow_array(
  77. q{SELECT cryptpassword FROM profiles WHERE userid = ?},
  78. undef, $user->id);
  79. $oldcryptedpwd || ThrowCodeError("unable_to_retrieve_password");
  80. my $oldpassword = $cgi->param('Bugzilla_password');
  81. # Wide characters cause crypt to die
  82. if (Bugzilla->params->{'utf8'}) {
  83. utf8::encode($oldpassword) if utf8::is_utf8($oldpassword);
  84. }
  85. if (crypt($oldpassword, $oldcryptedpwd) ne $oldcryptedpwd)
  86. {
  87. ThrowUserError("old_password_incorrect");
  88. }
  89. if ($pwd1 ne "" || $pwd2 ne "")
  90. {
  91. $cgi->param('new_password1')
  92. || ThrowUserError("new_password_missing");
  93. validate_password($pwd1, $pwd2);
  94. if ($cgi->param('Bugzilla_password') ne $pwd1) {
  95. my $cryptedpassword = bz_crypt($pwd1);
  96. $dbh->do(q{UPDATE profiles
  97. SET cryptpassword = ?
  98. WHERE userid = ?},
  99. undef, ($cryptedpassword, $user->id));
  100. # Invalidate all logins except for the current one
  101. Bugzilla->logout(LOGOUT_KEEP_CURRENT);
  102. }
  103. }
  104. }
  105. if ($user->authorizer->can_change_email
  106. && Bugzilla->params->{"allowemailchange"}
  107. && $cgi->param('new_login_name'))
  108. {
  109. my $old_login_name = $cgi->param('Bugzilla_login');
  110. my $new_login_name = trim($cgi->param('new_login_name'));
  111. if($old_login_name ne $new_login_name) {
  112. $cgi->param('Bugzilla_password')
  113. || ThrowUserError("old_password_required");
  114. use Bugzilla::Token;
  115. # Block multiple email changes for the same user.
  116. if (Bugzilla::Token::HasEmailChangeToken($user->id)) {
  117. ThrowUserError("email_change_in_progress");
  118. }
  119. # Before changing an email address, confirm one does not exist.
  120. validate_email_syntax($new_login_name)
  121. || ThrowUserError('illegal_email_address', {addr => $new_login_name});
  122. is_available_username($new_login_name)
  123. || ThrowUserError("account_exists", {email => $new_login_name});
  124. Bugzilla::Token::IssueEmailChangeToken($user, $old_login_name,
  125. $new_login_name);
  126. $vars->{'email_changes_saved'} = 1;
  127. }
  128. }
  129. my $realname = trim($cgi->param('realname'));
  130. trick_taint($realname); # Only used in a placeholder
  131. $dbh->do("UPDATE profiles SET realname = ? WHERE userid = ?",
  132. undef, ($realname, $user->id));
  133. }
  134. sub DoSettings {
  135. my $user = Bugzilla->user;
  136. my $settings = $user->settings;
  137. $vars->{'settings'} = $settings;
  138. my @setting_list = keys %$settings;
  139. $vars->{'setting_names'} = \@setting_list;
  140. $vars->{'has_settings_enabled'} = 0;
  141. # Is there at least one user setting enabled?
  142. foreach my $setting_name (@setting_list) {
  143. if ($settings->{"$setting_name"}->{'is_enabled'}) {
  144. $vars->{'has_settings_enabled'} = 1;
  145. last;
  146. }
  147. }
  148. $vars->{'dont_show_button'} = !$vars->{'has_settings_enabled'};
  149. }
  150. sub SaveSettings {
  151. my $cgi = Bugzilla->cgi;
  152. my $user = Bugzilla->user;
  153. my $settings = $user->settings;
  154. my @setting_list = keys %$settings;
  155. foreach my $name (@setting_list) {
  156. next if ! ($settings->{$name}->{'is_enabled'});
  157. my $value = $cgi->param($name);
  158. my $setting = new Bugzilla::User::Setting($name);
  159. if ($value eq "${name}-isdefault" ) {
  160. if (! $settings->{$name}->{'is_default'}) {
  161. $settings->{$name}->reset_to_default;
  162. }
  163. }
  164. else {
  165. $setting->validate_value($value);
  166. $settings->{$name}->set($value);
  167. }
  168. }
  169. $vars->{'settings'} = $user->settings(1);
  170. }
  171. sub DoEmail {
  172. my $dbh = Bugzilla->dbh;
  173. my $user = Bugzilla->user;
  174. ###########################################################################
  175. # User watching
  176. ###########################################################################
  177. if (Bugzilla->params->{"supportwatchers"}) {
  178. my $watched_ref = $dbh->selectcol_arrayref(
  179. "SELECT profiles.login_name FROM watch INNER JOIN profiles" .
  180. " ON watch.watched = profiles.userid" .
  181. " WHERE watcher = ?" .
  182. " ORDER BY profiles.login_name",
  183. undef, $user->id);
  184. $vars->{'watchedusers'} = $watched_ref;
  185. my $watcher_ids = $dbh->selectcol_arrayref(
  186. "SELECT watcher FROM watch WHERE watched = ?",
  187. undef, $user->id);
  188. my @watchers;
  189. foreach my $watcher_id (@$watcher_ids) {
  190. my $watcher = new Bugzilla::User($watcher_id);
  191. push (@watchers, Bugzilla::User::identity($watcher));
  192. }
  193. @watchers = sort { lc($a) cmp lc($b) } @watchers;
  194. $vars->{'watchers'} = \@watchers;
  195. }
  196. ###########################################################################
  197. # Role-based preferences
  198. ###########################################################################
  199. my $sth = $dbh->prepare("SELECT relationship, event " .
  200. "FROM email_setting " .
  201. "WHERE user_id = ?");
  202. $sth->execute($user->id);
  203. my %mail;
  204. while (my ($relationship, $event) = $sth->fetchrow_array()) {
  205. $mail{$relationship}{$event} = 1;
  206. }
  207. $vars->{'mail'} = \%mail;
  208. }
  209. sub SaveEmail {
  210. my $dbh = Bugzilla->dbh;
  211. my $cgi = Bugzilla->cgi;
  212. my $user = Bugzilla->user;
  213. if (Bugzilla->params->{"supportwatchers"}) {
  214. Bugzilla::User::match_field($cgi, { 'new_watchedusers' => {'type' => 'multi'} });
  215. }
  216. ###########################################################################
  217. # Role-based preferences
  218. ###########################################################################
  219. $dbh->bz_start_transaction();
  220. # Delete all the user's current preferences
  221. $dbh->do("DELETE FROM email_setting WHERE user_id = ?", undef, $user->id);
  222. # Repopulate the table - first, with normal events in the
  223. # relationship/event matrix.
  224. # Note: the database holds only "off" email preferences, as can be implied
  225. # from the name of the table - profiles_nomail.
  226. foreach my $rel (RELATIONSHIPS) {
  227. # Positive events: a ticked box means "send me mail."
  228. foreach my $event (POS_EVENTS) {
  229. if (defined($cgi->param("email-$rel-$event"))
  230. && $cgi->param("email-$rel-$event") == 1)
  231. {
  232. $dbh->do("INSERT INTO email_setting " .
  233. "(user_id, relationship, event) " .
  234. "VALUES (?, ?, ?)",
  235. undef, ($user->id, $rel, $event));
  236. }
  237. }
  238. # Negative events: a ticked box means "don't send me mail."
  239. foreach my $event (NEG_EVENTS) {
  240. if (!defined($cgi->param("neg-email-$rel-$event")) ||
  241. $cgi->param("neg-email-$rel-$event") != 1)
  242. {
  243. $dbh->do("INSERT INTO email_setting " .
  244. "(user_id, relationship, event) " .
  245. "VALUES (?, ?, ?)",
  246. undef, ($user->id, $rel, $event));
  247. }
  248. }
  249. }
  250. # Global positive events: a ticked box means "send me mail."
  251. foreach my $event (GLOBAL_EVENTS) {
  252. if (defined($cgi->param("email-" . REL_ANY . "-$event"))
  253. && $cgi->param("email-" . REL_ANY . "-$event") == 1)
  254. {
  255. $dbh->do("INSERT INTO email_setting " .
  256. "(user_id, relationship, event) " .
  257. "VALUES (?, ?, ?)",
  258. undef, ($user->id, REL_ANY, $event));
  259. }
  260. }
  261. $dbh->bz_commit_transaction();
  262. ###########################################################################
  263. # User watching
  264. ###########################################################################
  265. if (Bugzilla->params->{"supportwatchers"}
  266. && (defined $cgi->param('new_watchedusers')
  267. || defined $cgi->param('remove_watched_users')))
  268. {
  269. $dbh->bz_start_transaction();
  270. # Use this to protect error messages on duplicate submissions
  271. my $old_watch_ids =
  272. $dbh->selectcol_arrayref("SELECT watched FROM watch"
  273. . " WHERE watcher = ?", undef, $user->id);
  274. # The new information given to us by the user.
  275. my $new_watched_users = join(',', $cgi->param('new_watchedusers')) || '';
  276. my @new_watch_names = split(/[,\s]+/, $new_watched_users);
  277. my %new_watch_ids;
  278. foreach my $username (@new_watch_names) {
  279. my $watched_userid = login_to_id(trim($username), THROW_ERROR);
  280. $new_watch_ids{$watched_userid} = 1;
  281. }
  282. # Add people who were added.
  283. my $insert_sth = $dbh->prepare('INSERT INTO watch (watched, watcher)'
  284. . ' VALUES (?, ?)');
  285. foreach my $add_me (keys(%new_watch_ids)) {
  286. next if grep($_ == $add_me, @$old_watch_ids);
  287. $insert_sth->execute($add_me, $user->id);
  288. }
  289. if (defined $cgi->param('remove_watched_users')) {
  290. my @removed = $cgi->param('watched_by_you');
  291. # Remove people who were removed.
  292. my $delete_sth = $dbh->prepare('DELETE FROM watch WHERE watched = ?'
  293. . ' AND watcher = ?');
  294. my %remove_watch_ids;
  295. foreach my $username (@removed) {
  296. my $watched_userid = login_to_id(trim($username), THROW_ERROR);
  297. $remove_watch_ids{$watched_userid} = 1;
  298. }
  299. foreach my $remove_me (keys(%remove_watch_ids)) {
  300. $delete_sth->execute($remove_me, $user->id);
  301. }
  302. }
  303. $dbh->bz_commit_transaction();
  304. }
  305. }
  306. sub DoPermissions {
  307. my $dbh = Bugzilla->dbh;
  308. my $user = Bugzilla->user;
  309. my (@has_bits, @set_bits);
  310. my $groups = $dbh->selectall_arrayref(
  311. "SELECT DISTINCT name, description FROM groups WHERE id IN (" .
  312. $user->groups_as_string . ") ORDER BY name");
  313. foreach my $group (@$groups) {
  314. my ($nam, $desc) = @$group;
  315. push(@has_bits, {"desc" => $desc, "name" => $nam});
  316. }
  317. $groups = $dbh->selectall_arrayref('SELECT DISTINCT id, name, description
  318. FROM groups
  319. ORDER BY name');
  320. foreach my $group (@$groups) {
  321. my ($group_id, $nam, $desc) = @$group;
  322. if ($user->can_bless($group_id)) {
  323. push(@set_bits, {"desc" => $desc, "name" => $nam});
  324. }
  325. }
  326. # If the user has product specific privileges, inform him about that.
  327. foreach my $privs (PER_PRODUCT_PRIVILEGES) {
  328. next if $user->in_group($privs);
  329. $vars->{"local_$privs"} = $user->get_products_by_permission($privs);
  330. }
  331. $vars->{'has_bits'} = \@has_bits;
  332. $vars->{'set_bits'} = \@set_bits;
  333. }
  334. # No SavePermissions() because this panel has no changeable fields.
  335. sub DoSavedSearches {
  336. my $dbh = Bugzilla->dbh;
  337. my $user = Bugzilla->user;
  338. if ($user->queryshare_groups_as_string) {
  339. $vars->{'queryshare_groups'} =
  340. Bugzilla::Group->new_from_list($user->queryshare_groups);
  341. }
  342. $vars->{'bless_group_ids'} = [map {$_->{'id'}} @{$user->bless_groups}];
  343. }
  344. sub SaveSavedSearches {
  345. my $cgi = Bugzilla->cgi;
  346. my $dbh = Bugzilla->dbh;
  347. my $user = Bugzilla->user;
  348. # We'll need this in a loop, so do the call once.
  349. my $user_id = $user->id;
  350. my $sth_insert_nl = $dbh->prepare('INSERT INTO namedqueries_link_in_footer
  351. (namedquery_id, user_id)
  352. VALUES (?, ?)');
  353. my $sth_delete_nl = $dbh->prepare('DELETE FROM namedqueries_link_in_footer
  354. WHERE namedquery_id = ?
  355. AND user_id = ?');
  356. my $sth_insert_ngm = $dbh->prepare('INSERT INTO namedquery_group_map
  357. (namedquery_id, group_id)
  358. VALUES (?, ?)');
  359. my $sth_update_ngm = $dbh->prepare('UPDATE namedquery_group_map
  360. SET group_id = ?
  361. WHERE namedquery_id = ?');
  362. my $sth_delete_ngm = $dbh->prepare('DELETE FROM namedquery_group_map
  363. WHERE namedquery_id = ?');
  364. # Update namedqueries_link_in_footer for this user.
  365. foreach my $q (@{$user->queries}, @{$user->queries_available}) {
  366. if (defined $cgi->param("link_in_footer_" . $q->id)) {
  367. $sth_insert_nl->execute($q->id, $user_id) if !$q->link_in_footer;
  368. }
  369. else {
  370. $sth_delete_nl->execute($q->id, $user_id) if $q->link_in_footer;
  371. }
  372. }
  373. # For user's own queries, update namedquery_group_map.
  374. foreach my $q (@{$user->queries}) {
  375. my $group_id;
  376. if ($user->in_group(Bugzilla->params->{'querysharegroup'})) {
  377. $group_id = $cgi->param("share_" . $q->id) || '';
  378. }
  379. if ($group_id) {
  380. # Don't allow the user to share queries with groups he's not
  381. # allowed to.
  382. next unless grep($_ eq $group_id, @{$user->queryshare_groups});
  383. # $group_id is now definitely a valid ID of a group the
  384. # user can share queries with, so we can trick_taint.
  385. detaint_natural($group_id);
  386. if ($q->shared_with_group) {
  387. $sth_update_ngm->execute($group_id, $q->id);
  388. }
  389. else {
  390. $sth_insert_ngm->execute($q->id, $group_id);
  391. }
  392. # If we're sharing our query with a group we can bless, we
  393. # have the ability to add link to our search to the footer of
  394. # direct group members automatically.
  395. if ($user->can_bless($group_id) && $cgi->param('force_' . $q->id)) {
  396. my $group = new Bugzilla::Group($group_id);
  397. my $members = $group->members_non_inherited;
  398. foreach my $member (@$members) {
  399. next if $member->id == $user->id;
  400. $sth_insert_nl->execute($q->id, $member->id)
  401. if !$q->link_in_footer($member);
  402. }
  403. }
  404. }
  405. else {
  406. # They have unshared that query.
  407. if ($q->shared_with_group) {
  408. $sth_delete_ngm->execute($q->id);
  409. }
  410. # Don't remove namedqueries_link_in_footer entries for users
  411. # subscribing to the shared query. The idea is that they will
  412. # probably want to be subscribers again should the sharing
  413. # user choose to share the query again.
  414. }
  415. }
  416. $user->flush_queries_cache;
  417. # Update profiles.mybugslink.
  418. my $showmybugslink = defined($cgi->param("showmybugslink")) ? 1 : 0;
  419. $dbh->do("UPDATE profiles SET mybugslink = ? WHERE userid = ?",
  420. undef, ($showmybugslink, $user->id));
  421. $user->{'showmybugslink'} = $showmybugslink;
  422. }
  423. ###############################################################################
  424. # Live code (not subroutine definitions) starts here
  425. ###############################################################################
  426. my $cgi = Bugzilla->cgi;
  427. # This script needs direct access to the username and password CGI variables,
  428. # so we save them before their removal in Bugzilla->login, and delete them
  429. # before login in case we might be in a sudo session.
  430. my $bugzilla_login = $cgi->param('Bugzilla_login');
  431. my $bugzilla_password = $cgi->param('Bugzilla_password');
  432. $cgi->delete('Bugzilla_login', 'Bugzilla_password') if ($cgi->cookie('sudo'));
  433. Bugzilla->login(LOGIN_REQUIRED);
  434. $cgi->param('Bugzilla_login', $bugzilla_login);
  435. $cgi->param('Bugzilla_password', $bugzilla_password);
  436. $vars->{'changes_saved'} = $cgi->param('dosave');
  437. my $current_tab_name = $cgi->param('tab') || "settings";
  438. # The SWITCH below makes sure that this is valid
  439. trick_taint($current_tab_name);
  440. $vars->{'current_tab_name'} = $current_tab_name;
  441. my $token = $cgi->param('token');
  442. check_token_data($token, 'edit_user_prefs') if $cgi->param('dosave');
  443. # Do any saving, and then display the current tab.
  444. SWITCH: for ($current_tab_name) {
  445. /^account$/ && do {
  446. SaveAccount() if $cgi->param('dosave');
  447. DoAccount();
  448. last SWITCH;
  449. };
  450. /^settings$/ && do {
  451. SaveSettings() if $cgi->param('dosave');
  452. DoSettings();
  453. last SWITCH;
  454. };
  455. /^email$/ && do {
  456. SaveEmail() if $cgi->param('dosave');
  457. DoEmail();
  458. last SWITCH;
  459. };
  460. /^permissions$/ && do {
  461. DoPermissions();
  462. last SWITCH;
  463. };
  464. /^saved-searches$/ && do {
  465. SaveSavedSearches() if $cgi->param('dosave');
  466. DoSavedSearches();
  467. last SWITCH;
  468. };
  469. ThrowUserError("unknown_tab",
  470. { current_tab_name => $current_tab_name });
  471. }
  472. delete_token($token) if $cgi->param('dosave');
  473. if ($current_tab_name ne 'permissions') {
  474. $vars->{'token'} = issue_session_token('edit_user_prefs');
  475. }
  476. # Generate and return the UI (HTML page) from the appropriate template.
  477. print $cgi->header();
  478. $template->process("account/prefs/prefs.html.tmpl", $vars)
  479. || ThrowTemplateError($template->error());