diff options
-rw-r--r-- | Bugzilla/User.pm | 40 | ||||
-rw-r--r-- | CGI.pl | 2 | ||||
-rw-r--r-- | defparams.pl | 10 | ||||
-rwxr-xr-x | editusers.cgi | 1468 | ||||
-rw-r--r-- | globals.pl | 6 | ||||
-rw-r--r-- | skins/standard/admin.css | 28 | ||||
-rw-r--r-- | skins/standard/editusers.css | 52 | ||||
-rw-r--r-- | template/en/default/admin/groups/delete.html.tmpl | 2 | ||||
-rw-r--r-- | template/en/default/admin/users/confirm-delete.html.tmpl | 404 | ||||
-rw-r--r-- | template/en/default/admin/users/create.html.tmpl | 57 | ||||
-rw-r--r-- | template/en/default/admin/users/edit.html.tmpl | 154 | ||||
-rw-r--r-- | template/en/default/admin/users/list.html.tmpl | 98 | ||||
-rw-r--r-- | template/en/default/admin/users/listselectvars.html.tmpl | 34 | ||||
-rw-r--r-- | template/en/default/admin/users/search.html.tmpl | 70 | ||||
-rw-r--r-- | template/en/default/admin/users/userdata.html.tmpl | 79 | ||||
-rw-r--r-- | template/en/default/filterexceptions.pl | 25 | ||||
-rw-r--r-- | template/en/default/global/code-error.html.tmpl | 6 | ||||
-rw-r--r-- | template/en/default/global/messages.html.tmpl | 72 | ||||
-rw-r--r-- | template/en/default/global/user-error.html.tmpl | 14 |
19 files changed, 1812 insertions, 809 deletions
diff --git a/Bugzilla/User.pm b/Bugzilla/User.pm index f87f021b9..b17b638d1 100644 --- a/Bugzilla/User.pm +++ b/Bugzilla/User.pm @@ -86,6 +86,7 @@ sub _create { 'name' => '', 'login' => '', 'showmybugslink' => 0, + 'disabledtext' => '', 'flags' => {}, }; bless ($self, $class); @@ -101,9 +102,11 @@ sub _create { my ($id, $login, $name, + $disabledtext, $mybugslink) = $dbh->selectrow_array(qq{SELECT userid, login_name, realname, + disabledtext, mybugslink FROM profiles WHERE $cond}, @@ -115,6 +118,7 @@ sub _create { $self->{'id'} = $id; $self->{'name'} = $name; $self->{'login'} = $login; + $self->{'disabledtext'} = $disabledtext; $self->{'showmybugslink'} = $mybugslink; # Now update any old group information if needed @@ -951,12 +955,14 @@ sub get_userlist { return $self->{'userlist'}; } -sub insert_new_user ($$) { - my ($username, $realname) = (@_); +sub insert_new_user ($$;$$) { + my ($username, $realname, $password, $disabledtext) = (@_); my $dbh = Bugzilla->dbh; - # Generate a new random password for the user. - my $password = &::GenerateRandomPassword(); + $disabledtext ||= ''; + + # If not specified, generate a new random password for the user. + $password ||= &::GenerateRandomPassword(); my $cryptpassword = bz_crypt($password); # XXX - These should be moved into ValidateNewUser or CheckEmailSyntax @@ -966,10 +972,12 @@ sub insert_new_user ($$) { # Insert the new user record into the database. $dbh->do("INSERT INTO profiles - (login_name, realname, cryptpassword, emailflags) - VALUES (?, ?, ?, ?)", + (login_name, realname, cryptpassword, emailflags, + disabledtext) + VALUES (?, ?, ?, ?, ?)", undef, - ($username, $realname, $cryptpassword, DEFAULT_EMAIL_SETTINGS)); + ($username, $realname, $cryptpassword, DEFAULT_EMAIL_SETTINGS, + $disabledtext)); # Return the password to the calling code so it can be included # in an email sent to the user. @@ -1039,7 +1047,7 @@ Bugzilla::User - Object for a Bugzilla user my $user = new Bugzilla::User($id); # Class Functions - $random_password = insert_new_user($username, $realname); + $password = insert_new_user($username, $realname, $password, $disabledtext); =head1 DESCRIPTION @@ -1132,6 +1140,10 @@ linkinfooter - Whether or not the query should be displayed in the footer. =back +=item C<disabledtext> + +Returns the disable text of the user, if any. + =item C<flush_queries_cache> Some code modifies the set of stored queries. Because C<Bugzilla::User> does @@ -1254,12 +1266,18 @@ called "statically," just like a normal procedural function. =item C<insert_new_user> -Creates a new user in the database with a random password. +Creates a new user in the database. Params: $username (scalar, string) - The login name for the new user. $realname (scalar, string) - The full name for the new user. - -Returns: The password that we randomly generated for this user, in plain text. + $password (scalar, string) - Optional. The password for the new user; + if not given, a random password will be + generated. + $disabledtext (scalar, string) - Optional. The disable text for the new + user; if not given, it will be empty. + +Returns: The password for this user, in plain text, so it can be included + in an e-mail sent to the user. =item C<is_available_username> @@ -213,7 +213,7 @@ sub CheckEmailSyntax { my ($addr) = (@_); my $match = Param('emailregexp'); if ($addr !~ /$match/ || $addr =~ /[\\\(\)<>&,;:"\[\] \t\r\n]/) { - ThrowUserError("illegal_email_address", { addr => $addr }); + ThrowUserError("illegal_email_address", { addr => $addr }, 'abort'); } } diff --git a/defparams.pl b/defparams.pl index 169db1232..d5c46c9f4 100644 --- a/defparams.pl +++ b/defparams.pl @@ -1061,10 +1061,12 @@ Reason: %reason% { name => 'allowuserdeletion', - desc => 'The pages to edit users can also let you delete a user. But there ' . - 'is no code that goes and cleans up any references to that user in ' . - 'other tables, so such deletions are kinda scary. So, you have to ' . - 'turn on this option before any such deletions will ever happen.', + desc => 'The pages to edit users can also let you delete a user. ' . + 'Bugzilla will issue a warning in case you\'d run into ' . + 'inconsistencies when you\'re about to do so, ' . + 'but such deletions remain kinda scary. ' . + 'So, you have to turn on this option before any such deletions ' . + 'will ever happen.', type => 'b', default => 0 }, diff --git a/editusers.cgi b/editusers.cgi index 4c2a8a193..87a8b69bd 100755 --- a/editusers.cgi +++ b/editusers.cgi @@ -11,23 +11,9 @@ # implied. See the License for the specific language governing # rights and limitations under the License. # -# The Original Code is mozilla.org code. +# The Original Code is the Bugzilla Bug Tracking System. # -# The Initial Developer of the Original Code is Holger -# Schurig. Portions created by Holger Schurig are -# Copyright (C) 1999 Holger Schurig. All -# Rights Reserved. -# -# Contributor(s): Holger Schurig <holgerschurig@nikocity.de> -# Dave Miller <justdave@syndicomm.com> -# Joe Robins <jmrobins@tgix.com> -# Dan Mosedale <dmose@mozilla.org> -# Joel Peshkin <bugreport@peshkin.net> -# Erik Stambaugh <erik@dasbistro.com> -# -# Direct any questions on this source code to -# -# Holger Schurig <holgerschurig@nikocity.de> +# Contributor(s): Marc Schumann <wurblzap@gmail.com> use strict; use lib "."; @@ -35,827 +21,735 @@ use lib "."; require "CGI.pl"; require "globals.pl"; -use Bugzilla; +use vars qw( $vars ); + use Bugzilla::User; use Bugzilla::Constants; use Bugzilla::Auth; -# Shut up misguided -w warnings about "used only once". "use vars" just -# doesn't work for me. - -sub sillyness { - my $zz; - $zz = $::userid; -} - -my $editall; - - - -# TestUser: just returns if the specified user does exists -# CheckUser: same check, optionally emit an error text - -sub TestUser ($) -{ - my $user = shift; - - # does the product exist? - SendSQL("SELECT login_name - FROM profiles - WHERE login_name=" . SqlQuote($user)); - return FetchOneColumn(); -} - -sub CheckUser ($) -{ - my $user = shift; - - # do we have a user? - unless ($user) { - print "Sorry, you haven't specified a user."; - PutTrailer(); - exit; - } - - unless (TestUser $user) { - print "Sorry, user '$user' does not exist."; - PutTrailer(); - exit; - } -} +Bugzilla->login(LOGIN_REQUIRED); +my $cgi = Bugzilla->cgi(); +my $template = Bugzilla->template(); +my $dbh = Bugzilla->dbh; +my $user = Bugzilla->user(); +my $userid = $user->id(); +my $editusers = UserInGroup('editusers'); +my $action = $cgi->param('action') || 'search'; + +# Reject access if there is no sense in continuing. +$editusers + || Bugzilla->user->can_bless() + || ThrowUserError("auth_failure", {group => "editusers", + reason => "cant_bless", + action => "edit", + object => "users"}); +print Bugzilla->cgi->header(); -sub EmitElement ($$) -{ - my ($name, $value) = (@_); - $value = value_quote($value); - if ($editall) { - print qq{<TD><INPUT SIZE=64 MAXLENGTH=255 NAME="$name" VALUE="$value"></TD>\n}; +$vars->{'editusers'} = $editusers; +mirrorListSelectionValues(); + +########################################################################### +if ($action eq 'search') { + # Allow to restrict the search to any group the user is allowed to bless. + $vars->{'restrictablegroups'} = groupsUserMayBless($user, 'id', 'name'); + $template->process('admin/users/search.html.tmpl', $vars) + || ThrowTemplateError($template->error()); + +########################################################################### +} elsif ($action eq 'list') { + my $matchstr = $cgi->param('matchstr'); + my $matchtype = $cgi->param('matchtype'); + my $grouprestrict = $cgi->param('grouprestrict') || '0'; + my $groupid = $cgi->param('groupid'); + my $query = 'SELECT DISTINCT userid, login_name, realname, disabledtext ' . + 'FROM profiles'; + my @bindValues; + my $nextCondition; + + if (Param('usevisibilitygroups')) { + # Show only users in visible groups. + my $visibleGroups = visibleGroupsAsString(); + $query .= qq{, user_group_map AS ugm + WHERE ugm.user_id = profiles.userid + AND ugm.isbless = 0 + AND ugm.group_id IN ($visibleGroups) + }; + $nextCondition = 'AND'; } else { - print qq{<TD>$value<INPUT TYPE=HIDDEN NAME="$name" VALUE="$value"></TD>\n}; + if ($grouprestrict eq '1') { + $query .= ', user_group_map AS ugm'; + } + $nextCondition = 'WHERE'; } -} - -# -# Displays the form to edit a user parameters -# - -sub EmitFormElements ($$$$) -{ - my ($user_id, $user, $realname, $disabledtext) = @_; - - print " <TH ALIGN=\"right\">Login name:</TH>\n"; - EmitElement("user", $user); - - print "</TR><TR>\n"; - print " <TH ALIGN=\"right\">Real name:</TH>\n"; - EmitElement("realname", $realname); - - if ($editall) { - print "</TR><TR>\n"; - print " <TH ALIGN=\"right\">Password:</TH>\n"; - print qq| - <TD><INPUT TYPE="PASSWORD" SIZE="16" MAXLENGTH="16" NAME="password" VALUE=""><br> - (enter new password to change) - </TD> - |; - print "</TR><TR>\n"; - - print " <TH ALIGN=\"right\">Disable text:</TH>\n"; - print " <TD ROWSPAN=2><TEXTAREA NAME=\"disabledtext\" ROWS=10 COLS=60>" . - value_quote($disabledtext) . "</TEXTAREA>\n"; - print " </TD>\n"; - print "</TR><TR>\n"; - print " <TD VALIGN=\"top\">If non-empty, then the account will\n"; - print "be disabled, and this text should explain why.</TD>\n"; - } - - - if($user ne "") { - print "</TR><TR><TH VALIGN=TOP ALIGN=RIGHT>Group Access:</TH><TD><TABLE><TR>"; - SendSQL("SELECT groups.id, groups.name, groups.description, " . - "MAX(CASE WHEN grant_type = " . GRANT_DIRECT . " THEN 1 ELSE 0 END)," . - "MAX(CASE WHEN grant_type = " . GRANT_DERIVED . " THEN 1 ELSE 0 END)," . - "MAX(CASE WHEN grant_type = " . GRANT_REGEXP . " THEN 1 ELSE 0 END)" . - "FROM groups " . - "LEFT JOIN user_group_map " . - "ON user_group_map.group_id = groups.id " . - "AND isbless = 0 " . - "AND user_id = $user_id " . - "GROUP BY groups.name "); - if (MoreSQLData()) { - if ($editall) { - print "<TD COLSPAN=3 ALIGN=LEFT><B>Can turn this bit on for other users</B></TD>\n"; - print "</TR><TR>\n<TD ALIGN=CENTER><B>|</B></TD>\n"; - } - print "<TD COLSPAN=2 ALIGN=LEFT><B>User is a member of these groups</B></TD>\n"; - while (MoreSQLData()) { - my ($groupid, $name, $description, $checked, $isderived, $isregexp) = FetchSQLData(); - next unless ($editall || Bugzilla->user->can_bless($name)); - PushGlobalSQLState(); - SendSQL("SELECT user_id " . - "FROM user_group_map " . - "WHERE isbless = 1 " . - "AND user_id = $user_id " . - "AND group_id = $groupid"); - my ($blchecked) = FetchSQLData() ? 1 : 0; - SendSQL("SELECT grantor_id FROM user_group_map, - group_group_map - WHERE $groupid = grantor_id - AND user_group_map.user_id = $user_id - AND user_group_map.isbless = 0 - AND group_group_map.grant_type = " . GROUP_BLESS . " - AND user_group_map.group_id = member_id"); - my $derivedbless = FetchOneColumn(); - PopGlobalSQLState(); - print "</TR><TR"; - print ' bgcolor=#cccccc' if ($isderived || $isregexp); - print ">\n"; - print "<INPUT TYPE=HIDDEN NAME=\"oldgroup_$groupid\" VALUE=\"$checked\">\n"; - print "<INPUT TYPE=HIDDEN NAME=\"oldbless_$groupid\" VALUE=\"$blchecked\">\n"; - if ($editall) { - $blchecked = ($blchecked) ? "CHECKED" : ""; - print "<TD ALIGN=CENTER>"; - print "[" if $derivedbless; - print "<INPUT TYPE=CHECKBOX NAME=\"bless_$groupid\" $blchecked VALUE=\"$groupid\">"; - print "]" if $derivedbless; - print "</TD>\n"; - } - $checked = ($checked) ? "CHECKED" : ""; - print "<TD ALIGN=CENTER>"; - print '[' if ($isderived); - print '*' if ($isregexp); - print "<INPUT TYPE=CHECKBOX NAME=\"group_$groupid\" $checked VALUE=\"$groupid\">"; - print ']' if ($isderived); - print '*' if ($isregexp); - print "</TD><TD><B>"; - print ucfirst($name) . "</B>: $description</TD>\n"; - } + # Selection by user name. + if (defined($matchtype)) { + $query .= " $nextCondition profiles.login_name "; + if ($matchtype eq 'regexp') { + $query .= $dbh->sql_regexp . ' ?'; + $matchstr = '.' unless $matchstr; + } elsif ($matchtype eq 'notregexp') { + $query .= $dbh->sql_not_regexp . ' ?'; + $matchstr = '.' unless $matchstr; + } else { # substr or unknown + $query .= 'like ?'; + $matchstr = "%$matchstr%"; } - print "</TR></TABLE></TD>\n"; + $nextCondition = 'AND'; + # We can trick_taint because we use the value in a SELECT only, using + # a placeholder. + trick_taint($matchstr); + push(@bindValues, $matchstr); } -} - - -# -# Displays a text like "a.", "a or b.", "a, b or c.", "a, b, c or d." -# - -sub PutTrailer (@) -{ - my (@links) = ("Back to the <a href=\"./\">index</a>"); - if($editall) { - push(@links, - "<a href=\"editusers.cgi?action=add\">add</a> a new user"); + # Selection by group. + if ($grouprestrict eq '1') { + $query .= " $nextCondition profiles.userid = ugm.user_id " . + 'AND ugm.group_id = ?'; + # We can trick_taint because we use the value in a SELECT only, using + # a placeholder. + trick_taint($groupid); + push(@bindValues, $groupid); } - push(@links, @_); - - my $count = $#links; - my $num = 0; - print "<P>\n"; - foreach (@links) { - print $_; - if ($num == $count) { - print ".\n"; + $query .= ' ORDER BY profiles.login_name'; + + $vars->{'users'} = $dbh->selectall_arrayref($query, + {'Slice' => {}}, + @bindValues); + $template->process('admin/users/list.html.tmpl', $vars) + || ThrowTemplateError($template->error()); + +########################################################################### +} elsif ($action eq 'add') { + $editusers || ThrowUserError("auth_failure", {group => "editusers", + action => "add", + object => "users"}); + + $template->process('admin/users/create.html.tmpl', $vars) + || ThrowTemplateError($template->error()); + +########################################################################### +} elsif ($action eq 'new') { + $editusers || ThrowUserError("auth_failure", {group => "editusers", + action => "add", + object => "users"}); + + my $login = $cgi->param('login'); + my $password = $cgi->param('password'); + + # Cleanups + my $realname = trim($cgi->param('name') || ''); + my $disabledtext = trim($cgi->param('disabledtext') || ''); + + # Lock tables during the check+creation session. + $dbh->bz_lock_tables('profiles WRITE', + 'profiles_activity WRITE', + 'namedqueries READ', + 'whine_queries READ', + 'tokens READ'); + + # Validity checks + $login || ThrowUserError('user_login_required'); + CheckEmailSyntax($login); + is_available_username($login) || ThrowUserError('account_exists', + {'email' => $login}); + ValidatePassword($password); + + # Login and password are validated now, and realname and disabledtext + # are allowed to contain anything + trick_taint($login); + trick_taint($realname); + trick_taint($password); + trick_taint($disabledtext); + + insert_new_user($login, $realname, $password, $disabledtext); + my $userid = $dbh->bz_last_key('profiles', 'userid'); + $dbh->bz_unlock_tables(); + userDataToVars($userid); + + $vars->{'message'} = 'account_created'; + $template->process('admin/users/edit.html.tmpl', $vars) + || ThrowTemplateError($template->error()); + +########################################################################### +} elsif ($action eq 'edit') { + my $otherUser = new Bugzilla::User($cgi->param('userid')) + || ThrowCodeError('invalid_user_id', {'userid' => $cgi->param('userid')}); + my $otherUserID = $otherUser->id(); + + canSeeUser($otherUserID) + || ThrowUserError('auth_failure', {reason => "not_visible", + action => "modify", + object => "user"}); + + userDataToVars($otherUserID); + + $template->process('admin/users/edit.html.tmpl', $vars) + || ThrowTemplateError($template->error()); + +########################################################################### +} elsif ($action eq 'update') { + my $otherUser = new Bugzilla::User($cgi->param('userid')) + || ThrowCodeError('invalid_user_id', {'userid' => $cgi->param('userid')}); + my $otherUserID = $otherUser->id(); + my $logoutNeeded = 0; + my @changedFields; + + # Lock tables during the check+update session. + $dbh->bz_lock_tables('profiles WRITE', + 'profiles_activity WRITE', + 'fielddefs READ', + 'namedqueries READ', + 'whine_queries READ', + 'tokens WRITE', + 'logincookies WRITE', + 'groups READ', + 'user_group_map WRITE', + 'group_group_map READ'); + + canSeeUser($otherUserID) + || ThrowUserError('auth_failure', {reason => "not_visible", + action => "modify", + object => "user"}, + 'abort'); + + # Cleanups + my $login = trim($cgi->param('login') || ''); + my $loginold = $cgi->param('loginold') || ''; + my $realname = trim($cgi->param('name') || ''); + my $realnameold = $cgi->param('nameold') || ''; + my $password = $cgi->param('password') || ''; + my $disabledtext = trim($cgi->param('disabledtext') || ''); + my $disabledtextold = $cgi->param('disabledtextold') || ''; + + # Update profiles table entry; silently skip doing this if the user + # is not authorized. + if ($editusers) { + my @values; + + if ($login ne $loginold) { + # Validate, then trick_taint. + $login || ThrowUserError('user_login_required', undef, 'abort'); + CheckEmailSyntax($login); + is_available_username($login) || ThrowUserError('account_exists', + {'email' => $login}, + 'abort'); + trick_taint($login); + push(@changedFields, 'login_name'); + push(@values, $login); + $logoutNeeded = 1; + + # Since we change the login, silently delete any tokens. + $dbh->do('DELETE FROM tokens WHERE userid = ?', {}, $otherUserID); } - elsif ($num == $count-1) { - print " or "; + if ($realname ne $realnameold) { + # The real name may be anything; we use a placeholder for our + # INSERT, and we rely on displaying code to FILTER html. + trick_taint($realname); + push(@changedFields, 'realname'); + push(@values, $realname); } - else { - print ", "; + if ($password) { + # Validate, then trick_taint. + ValidatePassword($password) if $password; + trick_taint($password); + push(@changedFields, 'cryptpassword'); + push(@values, bz_crypt($password)); + $logoutNeeded = 1; } - $num++; - } - PutFooter(); -} - - - -# -# Preliminary checks: -# - -Bugzilla->login(LOGIN_REQUIRED); - -print Bugzilla->cgi->header(); - -$editall = UserInGroup("editusers"); - -$editall - || Bugzilla->user->can_bless - || ThrowUserError("auth_failure", {group => "editusers", - reason => "cant_bless", - action => "edit", - object => "users"}); - - -# -# often used variables -# -my $user = trim($::FORM{user} || ''); -my $action = trim($::FORM{action} || ''); -my $localtrailer = '<a href="editusers.cgi?">edit more users</a>'; -my $candelete = Param('allowuserdeletion'); - -my $dbh = Bugzilla->dbh; - -# -# action='' -> Ask for match string for users. -# - -unless ($action) { - PutHeader("Select match string"); - print qq{ -<FORM METHOD=GET ACTION="editusers.cgi"> -<INPUT TYPE=HIDDEN NAME="action" VALUE="list"> -List users with login name matching: -<INPUT SIZE=32 NAME="matchstr"> -<SELECT NAME="matchtype"> -<OPTION VALUE="substr" SELECTED>case-insensitive substring -<OPTION VALUE="regexp">case-sensitive regexp -<OPTION VALUE="notregexp">not (case-sensitive regexp) -</SELECT> -<BR> -<INPUT TYPE=SUBMIT VALUE="Submit"> -</FORM> -}; - PutTrailer(); - exit; -} - - -# -# action='list' -> Show nice list of matching users -# - -if ($action eq 'list') { - PutHeader("Select user"); - my $query = ""; - my $matchstr = $::FORM{'matchstr'}; - if (exists $::FORM{'matchtype'}) { - $query = "SELECT login_name,realname,disabledtext " . - "FROM profiles WHERE login_name "; - if ($::FORM{'matchtype'} eq 'substr') { - $query .= "like"; - $matchstr = '%' . $matchstr . '%'; - } elsif ($::FORM{'matchtype'} eq 'regexp') { - $query .= $dbh->sql_regexp(); - $matchstr = '.' - unless $matchstr; - } elsif ($::FORM{'matchtype'} eq 'notregexp') { - $query .= $dbh->sql_not_regexp(); - $matchstr = '.' - unless $matchstr; - } else { - die "Unknown match type"; - } - $query .= SqlQuote($matchstr) . " ORDER BY login_name"; - } elsif (exists $::FORM{'group'}) { - detaint_natural($::FORM{'group'}); - $query = "SELECT DISTINCT login_name,realname,disabledtext " . - "FROM profiles, user_group_map WHERE profiles.userid = user_group_map.user_id - AND group_id=" . $::FORM{'group'} . " ORDER BY login_name"; - } else { - die "Missing parameters"; - } - - SendSQL($query); - my $count = 0; - my $header = "<TABLE BORDER=1 CELLPADDING=4 CELLSPACING=0><TR BGCOLOR=\"#6666FF\"> -<TH ALIGN=\"left\">Edit user ...</TH> -<TH ALIGN=\"left\">Real name</TH> -"; - if ($candelete) { - $header .= "<TH ALIGN=\"left\">Action</TH>\n"; - } - $header .= "</TR>\n"; - print $header; - while ( MoreSQLData() ) { - $count++; - if ($count % 100 == 0) { - print "</table>$header"; - } - my ($user, $realname, $disabledtext) = FetchSQLData(); - my $s = ""; - my $e = ""; - if ($disabledtext) { - $s = '<span class="bz_inactive">'; - $e = '</span>'; + if ($disabledtext ne $disabledtextold) { + # The disable text may be anything; we use a placeholder for our + # INSERT, and we rely on displaying code to FILTER html. + trick_taint($disabledtext); + push(@changedFields, 'disabledtext'); + push(@values, $disabledtext); + $logoutNeeded = 1; } - $realname = ($realname ? html_quote($realname) : "<FONT COLOR=\"red\">missing</FONT>"); - print "<TR>\n"; - print " <TD VALIGN=\"top\"><A HREF=\"editusers.cgi?action=edit&user=", url_quote($user), "\"><B>$s", html_quote($user), "$e</B></A></TD>\n"; - print " <TD VALIGN=\"top\">$s$realname$e</TD>\n"; - if ($candelete) { - print " <TD VALIGN=\"top\"><A HREF=\"editusers.cgi?action=del&user=", url_quote($user), "\">Delete</A></TD>\n"; + if (@changedFields) { + push (@values, $otherUserID); + $logoutNeeded && Bugzilla->logout_user_by_id($otherUserID); + $dbh->do('UPDATE profiles SET ' . + join(' = ?,', @changedFields).' = ? ' . + 'WHERE userid = ?', + undef, @values); + # FIXME: should create profiles_activity entries. } - print "</TR>"; - } - if ($editall) { - print "<TR>\n"; - my $span = $candelete ? 3 : 2; - print qq{ -<TD VALIGN="top" COLSPAN=$span ALIGN="right"> - <A HREF=\"editusers.cgi?action=add\">add a new user</A> -</TD> -}; - print "</TR>"; } - print "</TABLE>\n"; - print "$count users found.\n"; - - PutTrailer($localtrailer); - exit; -} - - - - -# -# action='add' -> present form for parameters for new user -# -# (next action will be 'new') -# - -if ($action eq 'add') { - $editall || ThrowUserError("auth_failure", {group => "editusers", - action => "add", - object => "users"}); - PutHeader("Add user"); - print "<FORM METHOD=POST ACTION=editusers.cgi>\n"; - print "<TABLE BORDER=0 CELLPADDING=4 CELLSPACING=0><TR>\n"; - - EmitFormElements(0, '', '', ''); - - print "</TR></TABLE>\n<HR>\n"; - print "<INPUT TYPE=SUBMIT VALUE=\"Add\">\n"; - print "<INPUT TYPE=HIDDEN NAME=\"action\" VALUE=\"new\">\n"; - print "</FORM>"; - - my $other = $localtrailer; - $other =~ s/more/other/; - PutTrailer($other); - exit; -} + # Update group settings. + my $sth_add_mapping = $dbh->prepare( + qq{INSERT INTO user_group_map ( + user_id, group_id, isbless, grant_type + ) VALUES ( + ?, ?, ?, ? + ) + }); + my $sth_remove_mapping = $dbh->prepare( + qq{DELETE FROM user_group_map + WHERE user_id = ? + AND group_id = ? + AND isbless = ? + AND grant_type = ? + }); + + # We need the group names, too -- for display and for profiles_activity. + my $groups = $dbh->selectall_hashref('SELECT id, name FROM groups', 'id'); + my @groupsAddedTo; + my @groupsRemovedFrom; + my @groupsGrantedRightsToBless; + my @groupsDeniedRightsToBless; + + # Regard only groups the user is allowed to bless and skip all others + # silently. + # FIXME: checking for existence of each user_group_map entry + # would allow to display a friendlier error message on page reloads. + foreach (@{groupsUserMayBless($user, 'id')}) { + my $id = $$_{'id'}; + + # Change memberships. + my $oldgroupid = $cgi->param("oldgroup_$id") || '0'; + my $groupid = $cgi->param("group_$id") || '0'; + if ($groupid ne $oldgroupid) { + if ($groupid eq '0') { + $sth_remove_mapping->execute( + $otherUserID, $id, 0, GRANT_DIRECT); + push(@groupsRemovedFrom, $$groups{$id}{'name'}); + } else { + $sth_add_mapping->execute( + $otherUserID, $id, 0, GRANT_DIRECT); + push(@groupsAddedTo, $$groups{$id}{'name'}); + } + } - -# -# action='new' -> add user entered in the 'action=add' screen -# - -if ($action eq 'new') { - $editall || ThrowUserError("auth_failure", {group => "editusers", - action => "add", - object => "users"}); - - # Cleanups and valididy checks - my $realname = trim($::FORM{realname} || ''); - # We don't trim the password since that could falsely lead the user - # to believe a password with a space was accepted even though a space - # is an illegal character in a Bugzilla password. - my $password = $::FORM{'password'}; - my $disabledtext = trim($::FORM{disabledtext} || ''); - my $emailregexp = Param("emailregexp"); - - PutHeader("Adding new user"); - unless ($user) { - print "You must enter a name for the new user. Please press\n"; - print "<b>Back</b> and try again.\n"; - PutTrailer($localtrailer); - exit; - } - unless ($user =~ m/$emailregexp/) { - print "The user name entered must be a valid e-mail address. Please press\n"; - print "<b>Back</b> and try again.\n"; - PutTrailer($localtrailer); - exit; - } - if (!is_available_username($user)) { - print "The user '$user' does already exist. Please press\n"; - print "<b>Back</b> and try again.\n"; - PutTrailer($localtrailer); - exit; + # Only members of the editusers group may change bless grants. + # Skip silently if this is not the case. + if ($editusers) { + my $oldgroupid = $cgi->param("oldbless_$id") || '0'; + my $groupid = $cgi->param("bless_$id") || '0'; + if ($groupid ne $oldgroupid) { + if ($groupid eq '0') { + $sth_remove_mapping->execute( + $otherUserID, $id, 1, GRANT_DIRECT); + push(@groupsDeniedRightsToBless, $$groups{$id}{'name'}); + } else { + $sth_add_mapping->execute( + $otherUserID, $id, 1, GRANT_DIRECT); + push(@groupsGrantedRightsToBless, $$groups{$id}{'name'}); + } + } + } } - my $passworderror = ValidatePassword($password); - if ( $passworderror ) { - print $passworderror; - PutTrailer($localtrailer); - exit; + if (@groupsAddedTo || @groupsRemovedFrom) { + $dbh->do(qq{INSERT INTO profiles_activity ( + userid, who, + profiles_when, fieldid, + oldvalue, newvalue + ) VALUES ( + ?, ?, now(), ?, ?, ? + ) + }, + undef, + ($otherUserID, $userid, + GetFieldID('bug_group'), + join(', ', @groupsRemovedFrom), join(', ', @groupsAddedTo))); + $dbh->do('UPDATE profiles SET refreshed_when=? WHERE userid = ?', + undef, ('1900-01-01 00:00:00', $otherUserID)); } - - # Add the new user - SendSQL("INSERT INTO profiles ( " . - "login_name, cryptpassword, realname, " . - "emailflags, disabledtext" . - " ) VALUES ( " . - SqlQuote($user) . "," . - SqlQuote(bz_crypt($password)) . "," . - SqlQuote($realname) . "," . - SqlQuote(Bugzilla::Constants::DEFAULT_EMAIL_SETTINGS) . "," . - SqlQuote($disabledtext) . ")" ); - - #+++ send e-mail away - - print "OK, done.<br>\n"; - my $newuserid = $dbh->bz_last_key('profiles', 'userid'); - - my $changeduser = new Bugzilla::User($newuserid); - $changeduser->derive_groups(); - print "To change ${user}'s permissions, go back and " . - "<a href=\"editusers.cgi?action=edit&user=" . url_quote($user) . - "\">edit</a> this user."; - print "<p>\n"; - PutTrailer($localtrailer); - exit; - -} - - - -# -# action='del' -> ask if user really wants to delete -# -# (next action would be 'delete') -# - -if ($action eq 'del') { - $candelete || ThrowUserError("users_deletion_disabled"); - $editall || ThrowUserError("auth_failure", {group => "editusers", + # FIXME: should create profiles_activity entries for blesser changes. + + $dbh->bz_unlock_tables(); + + # FIXME: userDataToVars may be off when editing ourselves. + userDataToVars($otherUserID); + + $vars->{'message'} = 'account_updated'; + $vars->{'loginold'} = $loginold; + $vars->{'changed_fields'} = \@changedFields; + $vars->{'groups_added_to'} = \@groupsAddedTo; + $vars->{'groups_removed_from'} = \@groupsRemovedFrom; + $vars->{'groups_granted_rights_to_bless'} = \@groupsGrantedRightsToBless; + $vars->{'groups_denied_rights_to_bless'} = \@groupsDeniedRightsToBless; + $template->process('admin/users/edit.html.tmpl', $vars) + || ThrowTemplateError($template->error()); + +########################################################################### +} elsif ($action eq 'del') { + my $otherUser = new Bugzilla::User($cgi->param('userid')) + || ThrowCodeError('invalid_user_id', {'userid' => $cgi->param('userid')}); + my $otherUserID = $otherUser->id(); + + Param('allowuserdeletion') || ThrowUserError('users_deletion_disabled'); + $editusers || ThrowUserError('auth_failure', {group => "editusers", + action => "delete", + object => "users"}); + canSeeUser($otherUserID) || ThrowUserError('auth_failure', + {reason => "not_visible", action => "delete", - object => "users"}); - CheckUser($user); - - # display some data about the user - SendSQL("SELECT userid, realname FROM profiles - WHERE login_name=" . SqlQuote($user)); - my ($thisuserid, $realname) = - FetchSQLData(); - $realname = ($realname ? html_quote($realname) : "<FONT COLOR=\"red\">missing</FONT>"); - - PutHeader("Delete user $user"); - print "<TABLE BORDER=1 CELLPADDING=4 CELLSPACING=0>\n"; - print "<TR BGCOLOR=\"#6666FF\">\n"; - print " <TH VALIGN=\"top\" ALIGN=\"left\">Part</TH>\n"; - print " <TH VALIGN=\"top\" ALIGN=\"left\">Value</TH>\n"; - - print "</TR><TR>\n"; - print " <TD VALIGN=\"top\">Login name:</TD>\n"; - print " <TD VALIGN=\"top\">$user</TD>\n"; - - print "</TR><TR>\n"; - print " <TD VALIGN=\"top\">Real name:</TD>\n"; - print " <TD VALIGN=\"top\">$realname</TD>\n"; - - print "</TR><TR>\n"; - print " <TD VALIGN=\"top\">Group set:</TD>\n"; - print " <TD VALIGN=\"top\">"; - SendSQL("SELECT name - FROM groups, user_group_map - WHERE groups.id = user_group_map.group_id - AND user_group_map.user_id = $thisuserid - AND isbless = 0 - ORDER BY name"); - my $found = 0; - while ( MoreSQLData() ) { - my ($name) = FetchSQLData(); - print "<br>\n" if $found; - print ucfirst $name; - $found = 1; - } - print "none" unless $found; - print "</TD>\n</TR>"; - - - # Check if the user is an initialowner - my $nodelete = ''; - - SendSQL("SELECT products.name, components.name " . - "FROM products, components " . - "WHERE products.id = components.product_id " . - " AND initialowner=" . login_to_id($user)); - $found = 0; - while (MoreSQLData()) { - if ($found) { - print "<BR>\n"; - } else { - print "<TR>\n"; - print " <TD VALIGN=\"top\">Initial owner:</TD>\n"; - print " <TD VALIGN=\"top\">"; - } - my ($product, $component) = FetchSQLData(); - print "<a href=\"editcomponents.cgi?product=", url_quote($product), - "&component=", url_quote($component), - "&action=edit\">$product: $component</a>"; - $found = 1; - $nodelete = 'initial bug owner'; - } - print "</TD>\n</TR>" if $found; - - - # Check if the user is an initialqacontact - - SendSQL("SELECT products.name, components.name " . - "FROM products, components " . - "WHERE products.id = components.product_id " . - " AND initialqacontact=" . login_to_id($user)); - $found = 0; - while (MoreSQLData()) { - if ($found) { - print "<BR>\n"; - } else { - print "<TR>\n"; - print " <TD VALIGN=\"top\">Initial QA contact:</TD>\n"; - print " <TD VALIGN=\"top\">"; - } - my ($product, $component) = FetchSQLData(); - print "<a href=\"editcomponents.cgi?product=", url_quote($product), - "&component=", url_quote($component), - "&action=edit\">$product: $component</a>"; - $found = 1; - $nodelete = 'initial QA contact'; + object => "user"}); + + $vars->{'otheruser'} = $otherUser; + $vars->{'editcomponents'} = UserInGroup('editcomponents'); + + # If the user is initial owner or initial QA contact of a component, + # then no deletion is possible. + $vars->{'product_responsibilities'} = productResponsibilities($otherUserID); + + # Find other cross references. + $vars->{'bugs'} = $dbh->selectrow_array( + qq{SELECT COUNT(*) + FROM bugs + WHERE assigned_to = ? OR + qa_contact = ? OR + reporter = ? + }, + undef, ($otherUserID, $otherUserID, $otherUserID)); + $vars->{'cc'} = $dbh->selectrow_array( + 'SELECT COUNT(*) FROM cc WHERE who = ?', + undef, $otherUserID); + $vars->{'bugs_activity'} = $dbh->selectrow_array( + 'SELECT COUNT(*) FROM bugs_activity WHERE who = ?', + undef, $otherUserID); + $vars->{'flags'}{'requestee'} = $dbh->selectrow_array( + 'SELECT COUNT(*) FROM flags WHERE requestee_id = ?', + undef, $otherUserID); + $vars->{'flags'}{'setter'} = $dbh->selectrow_array( + 'SELECT COUNT(*) FROM flags WHERE setter_id = ?', + undef, $otherUserID); + $vars->{'groups'} = $dbh->selectall_arrayref( + qq{SELECT name + FROM groups, user_group_map + WHERE id = group_id + AND user_id = ? + AND isbless = 0 + ORDER BY name + }, + {'Slice' => {}}, $otherUserID); + $vars->{'longdescs'} = $dbh->selectrow_array( + 'SELECT COUNT(*) FROM longdescs WHERE who = ?', + undef, $otherUserID); + $vars->{'namedqueries'} = $dbh->selectrow_array( + 'SELECT COUNT(*) FROM namedqueries WHERE userid = ?', + undef, $otherUserID); + $vars->{'profiles_activity'} = $dbh->selectrow_array( + 'SELECT COUNT(*) FROM profiles_activity WHERE who = ? AND userid != ?', + undef, ($otherUserID, $otherUserID)); + $vars->{'series'} = $dbh->selectrow_array( + 'SELECT COUNT(*) FROM series WHERE creator = ?', + undef, $otherUserID); + $vars->{'votes'} = $dbh->selectrow_array( + 'SELECT COUNT(*) FROM votes WHERE who = ?', + undef, $otherUserID); + $vars->{'watch'}{'watched'} = $dbh->selectrow_array( + 'SELECT COUNT(*) FROM watch WHERE watched = ?', + undef, $otherUserID); + $vars->{'watch'}{'watcher'} = $dbh->selectrow_array( + 'SELECT COUNT(*) FROM watch WHERE watcher = ?', + undef, $otherUserID); + $vars->{'whine_events'} = $dbh->selectrow_array( + 'SELECT COUNT(*) FROM whine_events WHERE owner_userid = ?', + undef, $otherUserID); + $vars->{'whine_schedules'} = $dbh->selectrow_array( + qq{SELECT COUNT(distinct eventid) + FROM whine_schedules + WHERE mailto = ? + AND mailto_type = ? + }, + undef, ($otherUserID, MAILTO_USER)); + + $template->process('admin/users/confirm-delete.html.tmpl', $vars) + || ThrowTemplateError($template->error()); + +########################################################################### +} elsif ($action eq 'delete') { + my $otherUser = new Bugzilla::User($cgi->param('userid')) + || ThrowCodeError('invalid_user_id', {'userid' => $cgi->param('userid')}); + my $otherUserID = $otherUser->id(); + my $otherUserLogin = $otherUser->login(); + + # Lock tables during the check+removal session. + # FIXME: if there was some change on these tables after the deletion + # confirmation checks, we may do something here we haven't warned + # about. + $dbh->bz_lock_tables('products READ', + 'components READ', + 'logincookies WRITE', + 'profiles WRITE', + 'profiles_activity WRITE', + 'groups READ', + 'user_group_map WRITE', + 'group_group_map READ', + 'flags WRITE', + 'cc WRITE', + 'namedqueries WRITE', + 'tokens WRITE', + 'votes WRITE', + 'watch WRITE', + 'series WRITE', + 'series_data WRITE', + 'whine_schedules WRITE', + 'whine_queries WRITE', + 'whine_events WRITE'); + + Param('allowuserdeletion') + || ThrowUserError('users_deletion_disabled', undef, 'abort'); + $editusers || ThrowUserError('auth_failure', + {group => "editusers", + action => "delete", + object => "users"}, + 'abort'); + canSeeUser($otherUserID) || ThrowUserError('auth_failure', + {reason => "not_visible", + action => "delete", + object => "user"}, + 'abort'); + productResponsibilities($otherUserID) + && ThrowUserError('user_has_responsibility', undef, 'abort'); + + Bugzilla->logout_user_by_id($otherUserID); + + # Reference removals. + $dbh->do('UPDATE flags set requestee_id = NULL WHERE requestee_id = ?', + undef, $otherUserID); + + # Simple deletions in referred tables. + $dbh->do('DELETE FROM cc WHERE who = ?', undef, $otherUserID); + $dbh->do('DELETE FROM logincookies WHERE userid = ?', undef, $otherUserID); + $dbh->do('DELETE FROM namedqueries WHERE userid = ?', undef, $otherUserID); + $dbh->do('DELETE FROM profiles_activity WHERE userid = ? OR who = ?', undef, + ($otherUserID, $otherUserID)); + $dbh->do('DELETE FROM tokens WHERE userid = ?', undef, $otherUserID); + $dbh->do('DELETE FROM user_group_map WHERE user_id = ?', undef, + $otherUserID); + $dbh->do('DELETE FROM votes WHERE who = ?', undef, $otherUserID); + $dbh->do('DELETE FROM watch WHERE watcher = ? OR watched = ?', undef, + ($otherUserID, $otherUserID)); + + # More complex deletions in referred tables. + my $id; + + # 1) Series + my $sth_seriesid = $dbh->prepare( + 'SELECT series_id FROM series WHERE creator = ?'); + my $sth_deleteSeries = $dbh->prepare( + 'DELETE FROM series WHERE series_id = ?'); + my $sth_deleteSeriesData = $dbh->prepare( + 'DELETE FROM series_data WHERE series_id = ?'); + + $sth_seriesid->execute($otherUserID); + while ($id = $sth_seriesid->fetchrow_array()) { + $sth_deleteSeriesData->execute($id); + $sth_deleteSeries->execute($id); } - print "</TD>\n</TR>" if $found; - - print "</TABLE>\n"; + # 2) Whines + my $sth_whineidFromSchedules = $dbh->prepare( + qq{SELECT eventid FROM whine_schedules + WHERE mailto = ? AND mailto_type = ?}); + my $sth_whineidFromEvents = $dbh->prepare( + 'SELECT id FROM whine_events WHERE owner_userid = ?'); + my $sth_deleteWhineEvent = $dbh->prepare( + 'DELETE FROM whine_events WHERE id = ?'); + my $sth_deleteWhineQuery = $dbh->prepare( + 'DELETE FROM whine_queries WHERE eventid = ?'); + my $sth_deleteWhineSchedule = $dbh->prepare( + 'DELETE FROM whine_schedules WHERE eventid = ?'); + + $sth_whineidFromSchedules->execute($otherUserID, MAILTO_USER); + while ($id = $sth_whineidFromSchedules->fetchrow_array()) { + $sth_deleteWhineQuery->execute($id); + $sth_deleteWhineSchedule->execute($id); + $sth_deleteWhineEvent->execute($id); + } - if ($nodelete) { - print "<P>You can't delete this user because '$user' is an $nodelete ", - "for at least one product."; - PutTrailer($localtrailer); - exit; + $sth_whineidFromEvents->execute($otherUserID); + while ($id = $sth_whineidFromEvents->fetchrow_array()) { + $sth_deleteWhineQuery->execute($id); + $sth_deleteWhineSchedule->execute($id); + $sth_deleteWhineEvent->execute($id); } + # Finally, remove the user account itself. + $dbh->do('DELETE FROM profiles WHERE userid = ?', undef, $otherUserID); - print "<H2>Confirmation</H2>\n"; - print "<P>Do you really want to delete this user?<P>\n"; + $dbh->bz_unlock_tables(); - print "<FORM METHOD=POST ACTION=editusers.cgi>\n"; - print "<INPUT TYPE=SUBMIT VALUE=\"Yes, delete\">\n"; - print "<INPUT TYPE=HIDDEN NAME=\"action\" VALUE=\"delete\">\n"; - print "<INPUT TYPE=HIDDEN NAME=\"user\" VALUE=\"$user\">\n"; - print "</FORM>"; + $vars->{'message'} = 'account_deleted'; + $vars->{'otheruser'}{'login'} = $otherUserLogin; + $vars->{'restrictablegroups'} = groupsUserMayBless($user, 'id', 'name'); + $template->process('admin/users/search.html.tmpl', $vars) + || ThrowTemplateError($template->error()); - PutTrailer($localtrailer); - exit; +########################################################################### +} else { + $vars->{'action'} = $action; + ThrowCodeError('action_unrecognized', $vars); } +exit; +########################################################################### +# Helpers +########################################################################### -# -# action='delete' -> really delete the user -# - -if ($action eq 'delete') { - $candelete || ThrowUserError("users_deletion_disabled"); - $editall || ThrowUserError("auth_failure", {group => "editusers", - action => "delete", - object => "users"}); - CheckUser($user); - - SendSQL("SELECT userid - FROM profiles - WHERE login_name=" . SqlQuote($user)); - my $userid = FetchOneColumn(); - - Bugzilla->logout_user_by_id($userid); - SendSQL("DELETE FROM profiles - WHERE login_name=" . SqlQuote($user)); - SendSQL("DELETE FROM user_group_map - WHERE user_id=" . $userid); - - PutHeader("Deleting user"); - print "User deleted.<BR>\n"; - PutTrailer($localtrailer); - exit; +# Copy incoming list selection values from CGI params to template variables. +sub mirrorListSelectionValues { + if (defined($cgi->param('matchtype'))) { + foreach ('matchstr', 'matchtype', 'grouprestrict', 'groupid') { + $vars->{'listselectionvalues'}{$_} = $cgi->param($_); + } + } } +# Give a list of IDs of groups the user can see. +sub visibleGroupsAsString { + return join(', ', @{$user->visible_groups_direct()}); +} +# Give a list of IDs of groups the user may bless. +sub groupsUserMayBless { + my $user = shift; + my $fieldList = join(', ', @_); + my $query; + my $connector; + my @bindValues; -# -# action='edit' -> present the user edit from -# -# (next action would be 'update') -# - -if ($action eq 'edit') { - PutHeader("Edit user $user"); - CheckUser($user); - - # get data of user - SendSQL("SELECT userid, realname, disabledtext - FROM profiles - WHERE login_name=" . SqlQuote($user)); - my ($thisuserid, $realname, $disabledtext) = FetchSQLData(); + $user->derive_groups(1); - if ($thisuserid > 0) { - # Force groups to be up to date - my $changeduser = new Bugzilla::User($thisuserid); - $changeduser->derive_groups(); - } - print "<FORM METHOD=POST ACTION=editusers.cgi>\n"; - print "<TABLE BORDER=0 CELLPADDING=4 CELLSPACING=0><TR>\n"; - - EmitFormElements($thisuserid, $user, $realname, $disabledtext); - - print "</TR></TABLE>\n"; - print "<INPUT TYPE=HIDDEN NAME=\"userold\" VALUE=\"$user\">\n"; - print "<INPUT TYPE=HIDDEN NAME=\"realnameold\" VALUE=\"$realname\">\n"; - print "<INPUT TYPE=HIDDEN NAME=\"disabledtextold\" VALUE=\"" . - value_quote($disabledtext) . "\">\n"; - print "<INPUT TYPE=HIDDEN NAME=\"action\" VALUE=\"update\">\n"; - print "<INPUT TYPE=SUBMIT VALUE=\"Update\">\n"; - print "<BR>User is a member of any groups shown with a check or grey bar. - A grey bar indicates indirect membership, either derived from other - groups (marked with square brackets) or via regular expression - (marked with '*').<p> - Square brackets around the bless checkbox indicate the ability - to bless users (grant them membership in the group) as a result - of membership in another group. - <BR>"; - - print "</FORM>"; - if ($candelete) { - print "<FORM METHOD=POST ACTION=editusers.cgi>\n"; - print "<INPUT TYPE=SUBMIT VALUE=\"Delete User\">\n"; - print "<INPUT TYPE=HIDDEN NAME=\"action\" VALUE=\"del\">\n"; - print "<INPUT TYPE=HIDDEN NAME=\"user\" VALUE=\"$user\">\n"; - print "</FORM>"; + if ($editusers) { + $query = "SELECT DISTINCT $fieldList FROM groups"; + $connector = 'WHERE'; + } else { + $query = qq{SELECT DISTINCT $fieldList + FROM groups, user_group_map AS ugm + LEFT JOIN group_group_map AS ggm + ON ggm.member_id = ugm.group_id + AND ggm.grant_type = ? + WHERE user_id = ? + AND ((id = group_id AND isbless = 1) OR + (id = grantor_id)) + }; + @bindValues = (GROUP_BLESS, $userid); + $connector = 'AND'; } - my $x = $localtrailer; - $x =~ s/more/other/; - PutTrailer($x); - exit; -} - -# -# action='update' -> update the user -# - -if ($action eq 'update') { - my $userold = trim($::FORM{userold} || ''); - my $realname = trim($::FORM{realname} || ''); - my $realnameold = trim($::FORM{realnameold} || ''); - my $password = $::FORM{password} || ''; - my $disabledtext = trim($::FORM{disabledtext} || ''); - my $disabledtextold = trim($::FORM{disabledtextold} || ''); - my @localtrailers = ($localtrailer); - $localtrailer = qq|<a href="editusers.cgi?action=edit&user=XXX">edit user again</a>|; - PutHeader("Updating user $userold" . ($realnameold && " ($realnameold)")); - - CheckUser($userold); - SendSQL("SELECT userid FROM profiles - WHERE login_name=" . SqlQuote($userold)); - my ($thisuserid) = FetchSQLData(); - - my $emailregexp = Param("emailregexp"); - unless ($user =~ m/$emailregexp/) { - print "The user name entered must be a valid e-mail address. Please press\n"; - print "<b>Back</b> and try again.\n"; - PutTrailer($localtrailer); - exit; + # If visibilitygroups are used, restrict the set of groups. + if (Param('usevisibilitygroups')) { + my $visibleGroups = visibleGroupsAsString(); + $query .= " $connector id in ($visibleGroups)"; } - my @grpadd = (); - my @grpdel = (); - my $chggrp = 0; - SendSQL("SELECT id, name FROM groups"); - while (my ($groupid, $name) = FetchSQLData()) { - next unless ($editall || Bugzilla->user->can_bless($name)); - if ($::FORM{"oldgroup_$groupid"} != ($::FORM{"group_$groupid"} ? 1 : 0)) { - # group membership changed - PushGlobalSQLState(); - $chggrp = 1; - SendSQL("DELETE FROM user_group_map - WHERE user_id = $thisuserid - AND group_id = $groupid - AND isbless = 0 - AND grant_type = " . GRANT_DIRECT); - if ($::FORM{"group_$groupid"}) { - SendSQL("INSERT INTO user_group_map - (user_id, group_id, isbless, grant_type) - VALUES ($thisuserid, $groupid, 0," . GRANT_DIRECT . ")"); - print "Added user to group $name<BR>\n"; - push(@grpadd, $name); - } else { - print "Dropped user from group $name<BR>\n"; - push(@grpdel, $name); - } - PopGlobalSQLState(); - } - if ($editall && ($::FORM{"oldbless_$groupid"} != ($::FORM{"bless_$groupid"} ? 1 : 0))) { - # group membership changed - PushGlobalSQLState(); - SendSQL("DELETE FROM user_group_map - WHERE user_id = $thisuserid - AND group_id = $groupid - AND isbless = 1 - AND grant_type = " . GRANT_DIRECT); - if ($::FORM{"bless_$groupid"}) { - SendSQL("INSERT INTO user_group_map - (user_id, group_id, isbless, grant_type) - VALUES ($thisuserid, $groupid, 1," . GRANT_DIRECT . ")"); - print "Granted user permission to bless group $name<BR>\n"; - } else { - print "Revoked user's permission to bless group $name<BR>\n"; - } - PopGlobalSQLState(); - - } - } - my $fieldid = GetFieldID("bug_group"); - if ($chggrp) { - SendSQL("INSERT INTO profiles_activity " . - "(userid, who, profiles_when, fieldid, oldvalue, newvalue) " . - "VALUES " . "($thisuserid, $::userid, now(), $fieldid, " . - SqlQuote(join(", ",@grpdel)) . ", " . - SqlQuote(join(", ",@grpadd)) . ")"); - SendSQL("UPDATE profiles SET refreshed_when='1900-01-01 00:00:00' " . - "WHERE userid = $thisuserid"); - } + $query .= ' ORDER BY name'; + return $dbh->selectall_arrayref($query, {'Slice' => {}}, @bindValues); +} - # Update the database with the user's new password if they changed it. - if ( $editall && $password ) { - my $passworderror = ValidatePassword($password); - if ( !$passworderror ) { - my $cryptpassword = SqlQuote(bz_crypt($password)); - my $loginname = SqlQuote($userold); - SendSQL("UPDATE profiles - SET cryptpassword = $cryptpassword - WHERE login_name = $loginname"); - SendSQL("SELECT userid - FROM profiles - WHERE login_name=" . SqlQuote($userold)); - my $userid = FetchOneColumn(); - Bugzilla->logout_user_by_id($userid); - print "Updated password.<BR>\n"; - } else { - print "Did not update password: $passworderror<br>\n"; - } - } - if ($editall && $realname ne $realnameold) { - SendSQL("UPDATE profiles - SET realname=" . SqlQuote($realname) . " - WHERE login_name=" . SqlQuote($userold)); - print 'Updated real name to <q>' . html_quote($realname) . "</q>.<BR>\n"; - } - if ($editall && $disabledtext ne $disabledtextold) { - SendSQL("UPDATE profiles - SET disabledtext=" . SqlQuote($disabledtext) . " - WHERE login_name=" . SqlQuote($userold)); - SendSQL("SELECT userid - FROM profiles - WHERE login_name=" . SqlQuote($userold)); - my $userid = FetchOneColumn(); - Bugzilla->logout_user_by_id($userid); - print "Updated disabled text.<BR>\n"; +# Determine whether the user can see a user. (Checks for existence, too.) +sub canSeeUser { + my $otherUserID = shift; + my $query; + + if (Param('usevisibilitygroups')) { + my $visibleGroups = visibleGroupsAsString(); + $query = qq{SELECT COUNT(DISTINCT userid) + FROM profiles, user_group_map + WHERE userid = ? + AND user_id = userid + AND isbless = 0 + AND group_id IN ($visibleGroups) + }; + } else { + $query = qq{SELECT COUNT(userid) + FROM profiles + WHERE userid = ? + }; } - if ($editall && $user ne $userold) { - unless ($user) { - print "Sorry, I can't delete the user's name."; - $userold = url_quote($userold); - $localtrailer =~ s/XXX/$userold/; - push @localtrailers, $localtrailer; - PutTrailer(@localtrailers); - exit; - } - if (TestUser($user)) { - print "Sorry, user name '$user' is already in use."; - $userold = url_quote($userold); - $localtrailer =~ s/XXX/$userold/; - push @localtrailers, $localtrailer; - PutTrailer($localtrailer); - exit; - } - - SendSQL("UPDATE profiles - SET login_name=" . SqlQuote($user) . " - WHERE login_name=" . SqlQuote($userold)); + return $dbh->selectrow_array($query, undef, $otherUserID); +} - print q|Updated user's name to <a href="mailto:| . - url_quote($user) . '">' . html_quote($user) . "</a>.<BR>\n"; +# Retrieve product responsibilities, usable for both display and verification. +sub productResponsibilities { + my $userid = shift; + my $h = $dbh->selectall_arrayref( + qq{SELECT products.name AS productname, + components.name AS componentname, + initialowner, + initialqacontact + FROM products, components + WHERE products.id = components.product_id + AND ? IN (initialowner, initialqacontact) + }, + {'Slice' => {}}, $userid); + + if (@$h) { + return $h; + } else { + return undef; } - my $changeduser = new Bugzilla::User($thisuserid); - $changeduser->derive_groups(); - - $user = url_quote($user); - $localtrailer =~ s/XXX/$user/; - push @localtrailers, $localtrailer; - PutTrailer(@localtrailers); - exit; } - - -# -# No valid action found -# - -PutHeader("Error"); -print "I don't have a clue what you want.<BR>\n"; +# Retrieve user data for the user editing form. User creation and user +# editing code rely on this to call derive_groups(). +sub userDataToVars { + my $userid = shift; + my $user = new Bugzilla::User($userid); + my $query; + + $user->derive_groups(); + + $vars->{'otheruser'} = $user; + $vars->{'groups'} = groupsUserMayBless($user, 'id', 'name', 'description'); + $vars->{'disabledtext'} = $dbh->selectrow_array( + 'SELECT disabledtext FROM profiles WHERE userid = ?', undef, $userid); + + $vars->{'permissions'} = $dbh->selectall_hashref( + qq{SELECT id, + COUNT(directmember.group_id) AS directmember, + COUNT(regexpmember.group_id) AS regexpmember, + COUNT(derivedmember.group_id) AS derivedmember, + COUNT(directbless.group_id) AS directbless + FROM groups + LEFT JOIN user_group_map AS directmember + ON directmember.group_id = id + AND directmember.user_id = ? + AND directmember.isbless = 0 + AND directmember.grant_type = ? + LEFT JOIN user_group_map AS regexpmember + ON regexpmember.group_id = id + AND regexpmember.user_id = ? + AND regexpmember.isbless = 0 + AND regexpmember.grant_type = ? + LEFT JOIN user_group_map AS derivedmember + ON derivedmember.group_id = id + AND derivedmember.user_id = ? + AND derivedmember.isbless = 0 + AND derivedmember.grant_type = ? + LEFT JOIN user_group_map AS directbless + ON directbless.group_id = id + AND directbless.user_id = ? + AND directbless.isbless = 1 + AND directbless.grant_type = ? + GROUP BY id + }, + 'id', undef, + ($userid, GRANT_DIRECT, + $userid, GRANT_REGEXP, + $userid, GRANT_DERIVED, + $userid, GRANT_DIRECT)); + + # Find indirect bless permission. + $query = qq{SELECT groups.id + FROM groups, user_group_map AS ugm, group_group_map AS ggm + WHERE ugm.user_id = ? + AND groups.id = ggm.grantor_id + AND ggm.member_id = ugm.group_id + AND ugm.isbless = 0 + AND ggm.grant_type = ? + GROUP BY id + }; + foreach (@{$dbh->selectall_arrayref($query, undef, ($userid, GROUP_BLESS))}) { + # Merge indirect bless permissions into permission variable. + $vars->{'permissions'}{${$_}[0]}{'indirectbless'} = 1; + } +} diff --git a/globals.pl b/globals.pl index 721f3bc43..e71493f6b 100644 --- a/globals.pl +++ b/globals.pl @@ -613,11 +613,11 @@ sub ValidatePassword { my ($password, $matchpassword) = @_; if (length($password) < 3) { - ThrowUserError("password_too_short"); + ThrowUserError("password_too_short", undef, 'abort'); } elsif (length($password) > 16) { - ThrowUserError("password_too_long"); + ThrowUserError("password_too_long", undef, 'abort'); } elsif ((defined $matchpassword) && ($password ne $matchpassword)) { - ThrowUserError("passwords_dont_match"); + ThrowUserError("passwords_dont_match", undef, 'abort'); } } diff --git a/skins/standard/admin.css b/skins/standard/admin.css new file mode 100644 index 000000000..1f266b792 --- /dev/null +++ b/skins/standard/admin.css @@ -0,0 +1,28 @@ +/* The contents of this file are subject to the Mozilla Public + * License Version 1.1 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS + * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or + * implied. See the License for the specific language governing + * rights and limitations under the License. + * + * The Original Code is the Bugzilla Bug Tracking System. + * + * Contributor(s): Marc Schumann <wurblzap@gmail.com> + */ + +ul.warningmessages { + background-color: white; + border-style: solid; + border-width: 1px; + border-color: yellow; + padding: 1ex 1ex 1ex 4ex; +} + +p.areyoureallyreallysure { + color: red; + font-size: 120%; + font-weight: bold; +} diff --git a/skins/standard/editusers.css b/skins/standard/editusers.css new file mode 100644 index 000000000..a5bf4581f --- /dev/null +++ b/skins/standard/editusers.css @@ -0,0 +1,52 @@ +/* The contents of this file are subject to the Mozilla Public + * License Version 1.1 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS + * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or + * implied. See the License for the specific language governing + * rights and limitations under the License. + * + * The Original Code is the Bugzilla Bug Tracking System. + * + * Contributor(s): Marc Schumann <wurblzap@gmail.com> + */ + +table.main { + border-spacing: 1em; +} +table.main tr { + vertical-align: top; + border-top: solid thin black; +} +table.main th { + text-align: right; + white-space: nowrap; +} +table.main th, +table.main td { + padding: 0; +} +table.main ul { + list-style-type: none; + padding-left: 0 +} + +table.groups { + border-spacing: 1px; +} +table.groups tr.indirect { + background-color: #cccccc; +} +table.groups th { + text-align: left; + padding: 0 0 0 1ex; +} +table.groups td { + padding: 2px; +} +table.groups td.checkbox { + text-align: center; + white-space: nowrap; +} diff --git a/template/en/default/admin/groups/delete.html.tmpl b/template/en/default/admin/groups/delete.html.tmpl index 842e2c6f1..2c4b70862 100644 --- a/template/en/default/admin/groups/delete.html.tmpl +++ b/template/en/default/admin/groups/delete.html.tmpl @@ -56,7 +56,7 @@ <p><b>One or more users belong to this group. You cannot delete this group while there are users in it.</b> - <br><a href="editusers.cgi?action=list&group=[% gid FILTER html %]">Show + <br><a href="editusers.cgi?action=list&group=[% gid FILTER html %]&grouprestrict=1">Show me which users</a> - <input type="checkbox" name="removeusers">Remove all users from this group for me.</p> [% END %] diff --git a/template/en/default/admin/users/confirm-delete.html.tmpl b/template/en/default/admin/users/confirm-delete.html.tmpl new file mode 100644 index 000000000..ece5de7e0 --- /dev/null +++ b/template/en/default/admin/users/confirm-delete.html.tmpl @@ -0,0 +1,404 @@ +[%# 1.0@bugzilla.org %] +[%# The contents of this file are subject to the Mozilla Public + # License Version 1.1 (the "License"); you may not use this file + # except in compliance with the License. You may obtain a copy of + # the License at http://www.mozilla.org/MPL/ + # + # Software distributed under the License is distributed on an "AS + # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or + # implied. See the License for the specific language governing + # rights and limitations under the License. + # + # The Original Code is the Bugzilla Bug Tracking System. + # + # Contributor(s): Marc Schumann <wurblzap@gmail.com> + #%] + +[%# INTERFACE: + # + # listselectionvalues: selection values to recreate the current user + # list. + # editusers: is viewing user member of editusers? + # editcomponents: is viewing user member of editcomponents? + # otheruser: Bugzilla::User object of the viewed user. + # groups: array of Group names the viewed user is a member + # of. + # product_responsibilities: list of hashes, one entry per Bugzilla component. + # productname: Name of the product. + # componentname: Name of the component. + # initialowner: User ID of initial owner. + # initialqacontact: User ID of initial QA contact. + # bugs: number of bugs the viewed user has a role in + # bug_activity: number of bugs the viewed user has activity + # entries on + # cc number of bugs the viewed user is cc list member + # of + # flags.requestee: number of flags the viewed user is being asked for + # flags.setter: number of flags the viewed user has set + # longdescs: number of bug comments the viewed user has written + # namedqueries: number of named queries the user has created + # profiles_activity: number of named queries the user has created + # series: number of series the viewed user has created + # votes: number of bugs the viewed user has voted on + # watch.watched: number of users the viewed user is being watched + # by + # watch.watcher: number of users the viewed user is watching + # whine_events: number of whine events the viewed user has created + # whine_schedules: number of whine schedules the viewed user has + # created + #%] + +[% PROCESS global/header.html.tmpl + title = "Confirm deletion of user $otheruser.login" + style_urls = ['skins/standard/admin.css', + 'skins/standard/editusers.css'] +%] + +[% PROCESS admin/users/listselectvars.html.tmpl + listselectionvalues = listselectionvalues +%] + +[% responsibilityterms = { + 'initialowner' => 'Initial Owner', + 'initialqacontact' => 'Initial QA Contact' + } +%] + +<table class="main"> + <tr> + <th>Login name:</th> + <td>[% otheruser.login FILTER html %]</td> + </tr> + <tr> + <th>Real name:</th> + <td>[% otheruser.name FILTER html %]</td> + </tr> + <tr> + <th>Group set:</th> + <td> + [% IF groups.size %] + <ul> + [% FOREACH group = groups %] + <li>[% group.name FILTER html %]</li> + [% END %] + </ul> + [% ELSE %] + None + [% END %] + </td> + </tr> + [% IF product_responsibilities.size %] + <tr> + <th>Product responsibilities:</th> + <td> + <ul> + [% FOREACH component = product_responsibilities %] + <li> + [% andstring = '' %] + [% FOREACH responsibility = ['initialowner', 'initialqacontact'] %] + [% IF component.$responsibility == userid %] + [% andstring %] [% responsibilityterms.$responsibility %] + [% andstring = ' and ' %] + [% END %] + [% END %] + for + [% IF editcomponents %] + <a href="editcomponents.cgi?action=edit&product= + [% component.productname FILTER url_quote %]&component= + [% component.componentname FILTER url_quote %]"> + [% END %] + [%+ component.productname FILTER html %]: + [% component.componentname FILTER html %] + [% IF editcomponents %] + </a> + [% END %] + </li> + [% END %] + </ul> + </td> + </tr> + [% END %] +</table> + +[% IF product_responsibilities.size %] + <p> + You can't delete this user at this time because + [%+ otheruser.login FILTER html %] has got responsibilities for at least + one product. + </p> + <p> + [% IF editcomponents %] + Change this by clicking the product editing links above, + [% ELSE %] + For now, you can + [% END %] +[% ELSE %] + + <h2>Confirmation</h2> + + [% IF bugs || bug_activity || cc || flags.requestee || flags.setter || + longdescs || namedqueries || profiles_activity || series || votes || + watch.watched || watch.watcher || whine_events || whine_schedules %] + <ul class="warningmessages"> + [% IF bugs %] + <li> + [% otheruser.login FILTER html %] + <a href="buglist.cgi?emailassigned_to1=1&emailreporter1=1&emailqa_contact1=1&emailtype1=exact&email1=[% otheruser.login FILTER url_quote %]">is + related to + [% IF bugs == 1 %] + [%+ terms.abug %] + [% ELSE %] + [%+ bugs %] [%+ terms.bugs %] + [% END %]</a>, by having reported, being assigned to or being + the QA contact. + If you delete the user account, the [% terms.bugs %] table in the + database will be inconsistent, resulting in + [% IF bugs == 1 %] + this [% terms.bug %] + [% ELSE %] + these [% terms.bugs %] + [% END %] + not appearing in [% terms.bug %] lists any more. + </li> + [% END %] + [% IF bugs_activity %] + <li> + [% otheruser.login FILTER html %] has made + [% IF bugs_activity == 1 %] + a change on [% terms.abug %] + [% ELSE %] + changes on [% terms.bugs %] + [% END %]. + If you delete the user account, the [% terms.bugs %] activity table in + the database will be inconsistent, resulting in + [% IF bugs_activity == 1 %] + this change + [% ELSE %] + these changes + [% END %] + not showing up in [% terms.bug %] activity logs any more. + </li> + [% END %] + [% IF cc %] + <li> + [% otheruser.login FILTER html %] + <a href="buglist.cgi?emailcc1=1&emailtype1=exact&email1=[% otheruser.login FILTER url_quote %]">is + on the CC list of + [% IF cc == 1 %] + [%+ terms.abug %] + [% ELSE %] + [%+ cc %] [%+ terms.bugs %] + [% END %]</a>. + If you delete the user account, it will be removed from these CC + lists. + </li> + [% END %] + [% IF flags.requestee %] + <li> + [% otheruser.login FILTER html %] has been + <a href="buglist.cgi?field0-0-0=requestees.login_name&type0-0-0=equals&value0-0-0=[% otheruser.login FILTER url_quote %]">asked + to set + [% IF flags.requestee == 1 %] + a flag + [% ELSE %] + [% flags.requestee %] flags + [% END %]</a>. + If you delete the user account, + [% IF flags.requestee == 1 %] + this flag + [% ELSE %] + these flags + [% END %] + will change to be unspecifically requested. + </li> + [% END %] + [% IF flags.setter %] + <li> + [% otheruser.login FILTER html %] has + <a href="buglist.cgi?field0-0-0=setters.login_name&type0-0-0=equals&value0-0-0=[% otheruser.login FILTER url_quote %]">set + or requested + [% IF flags.setter == 1 %] + a flag + [% ELSE %] + [%+ flags.setter %] flags + [% END %]</a>. + If you delete the user account, the flags table in the database + will be inconsistent, resulting in + [% IF flags.setter == 1 %] + this flag + [% ELSE %] + these flags + [% END %] + not displaying correctly any more. + </li> + [% END %] + [% IF longdescs %] + <li> + [% otheruser.login FILTER html %] has + <a href="buglist.cgi?emaillongdesc1=1&emailtype1=exact&email1=[% otheruser.login FILTER url_quote %]">commented + [% IF longdescs == 1 %] + once on [% terms.abug %] + [% ELSE %] + [%+ longdescs %] times on [% terms.bugs %] + [% END %]</a>. + If you delete the user account, the comments table in the database + will be inconsistent, resulting in + [% IF longdescs == 1 %] + this comment + [% ELSE %] + these comments + [% END %] + not being visible any more. + </li> + [% END %] + [% IF namedqueries %] + <li> + [% otheruser.login FILTER html %] + has + [% IF namedqueries == 1 %] + a named query + [% ELSE %] + [%+ namedqueries %] named queries + [% END %]. + [% IF namedqueries == 1 %] + This named query + [% ELSE %] + These named queries + [% END %] + will be deleted along with the user account. + </li> + [% END %] + [% IF profiles_activity %] + <li> + [% otheruser.login FILTER html %] has made + [% IF bugs_activity == 1 %] + a change on a other user's profile + [% ELSE %] + changes on other users' profiles + [% END %]. + If you delete the user account, the user profiles activity table in + the database will be inconsistent. + </li> + [% END %] + [% IF series %] + <li> + [% otheruser.login FILTER html %] has created + [% IF series == 1 %] + a series + [% ELSE %] + [%+ series %] series + [% END %]. + [% IF series == 1 %] + This series + [% ELSE %] + These series + [% END %] + will be deleted along with the user account. + </li> + [% END %] + [% IF votes %] + <li> + [% otheruser.login FILTER html %] has voted on + [% IF votes == 1 %] + [%+ terms.abug %] + [% ELSE %] + [%+ votes %] [%+ terms.bugs %] + [% END %]. + If you delete the user account, + [% IF votes == 1 %] + this vote + [% ELSE %] + these votes + [% END %] + will be deleted along with the user account. + </li> + [% END %] + [% IF watch.watched || watch.watcher %] + <li> + [% otheruser.login FILTER html %] + [% IF watch.watched %] + is being watched by + [% IF watch.watched == 1 %] + a user + [% ELSE %] + [%+ watch.watched %] users + [% END %] + [% END %] + [% IF watch.watcher %] + [%+ 'and' IF watch.watched %] + watches + [% IF watch.watcher == 1 %] + a user + [% ELSE %] + [%+ watch.watcher %] users + [% END %] + [% END %]. + [% IF watch.watched + watch.watcher == 1 %] + This watching + [% ELSE %] + These watchings + [% END %] + will cease along with the deletion of the user account. + </li> + [% END %] + [% IF whine_events || whine_schedules %] + <li> + [% otheruser.login FILTER html %] + [% IF whine_events %] + has scheduled + [% IF whine_events == 1 %] + a whine + [% ELSE %] + [%+ whine_events %] whines + [% END %] + [% END %] + [% IF whine_schedules %] + [%+ 'and' IF whine_events %] + is on the receiving end of + [% IF whine_schedules == 1 %] + a whine + [% ELSE %] + [%+ whine_schedules %] whines + [% END %] + [% END %]. + [% IF whine_events + whine_schedules == 1 %] + This whine + [% ELSE %] + These whines + [% END %] + will be deleted along with the user account. + </li> + [% END %] + </ul> + <p class="areyoureallyreallysure"> + Please be aware of the consequences of this before continuing. + </p> + [% END %] + + <p>Do you really want to delete this user account?</p> + + <form method="post" action="editusers.cgi"> + <p> + <input type="submit" value="Yes, delete" /> + <input type="hidden" name="action" value="delete" /> + <input type="hidden" name="userid" value="[% otheruser.id %]" /> + [% INCLUDE listselectionhiddenfields %] + </p> + </form> + + <p>If you do not want to delete the user account at this time, +[% END %] + + <a href="editusers.cgi?action=edit&userid=[% otheruser.id %] + [% INCLUDE listselectionurlparams %]">edit the user</a>, + go + <a href="editusers.cgi?action=list[% INCLUDE listselectionurlparams %]">back + to the user list</a>, + [% IF editusers %] + <a href="editusers.cgi?action=add[% INCLUDE listselectionurlparams %]">add + a new user</a>, + [% END %] + or <a href="editusers.cgi">find other users</a>. +</p> + +[% PROCESS global/footer.html.tmpl %] diff --git a/template/en/default/admin/users/create.html.tmpl b/template/en/default/admin/users/create.html.tmpl new file mode 100644 index 000000000..42aefbdb7 --- /dev/null +++ b/template/en/default/admin/users/create.html.tmpl @@ -0,0 +1,57 @@ +[%# 1.0@bugzilla.org %] +[%# The contents of this file are subject to the Mozilla Public + # License Version 1.1 (the "License"); you may not use this file + # except in compliance with the License. You may obtain a copy of + # the License at http://www.mozilla.org/MPL/ + # + # Software distributed under the License is distributed on an "AS + # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or + # implied. See the License for the specific language governing + # rights and limitations under the License. + # + # The Original Code is the Bugzilla Bug Tracking System. + # + # Contributor(s): Marc Schumann <wurblzap@gmail.com> + #%] + +[%# INTERFACE: + # + # listselectionvalues: selection values to recreate the current user list. + # editusers: is viewing user member of editusers? + #%] + +[% PROCESS global/header.html.tmpl + title = "Add user" + style_urls = ['skins/standard/editusers.css'] + onload = "document.forms['f'].login.focus()" +%] + +[% PROCESS admin/users/listselectvars.html.tmpl + listselectionvalues = listselectionvalues +%] + +<form name="f" method="post" action="editusers.cgi"> +<table class="main"> + [% PROCESS admin/users/userdata.html.tmpl + editform = 0 + editusers = editusers + otheruser = [] + %] +</table> +<p> + <input type="submit" value="Add" /> + <input type="hidden" name="action" value="new" /> + [% INCLUDE listselectionhiddenfields %] +</p> +</form> + +<p> + You can also <a href="editusers.cgi">find a user</a> + [% IF listselectionvalues %], + or + <a href="editusers.cgi?action=list[% INCLUDE listselectionurlparams %]">go + back to the user list</a> + [% END %]. +</p> + +[% PROCESS global/footer.html.tmpl %] diff --git a/template/en/default/admin/users/edit.html.tmpl b/template/en/default/admin/users/edit.html.tmpl new file mode 100644 index 000000000..53ea17f21 --- /dev/null +++ b/template/en/default/admin/users/edit.html.tmpl @@ -0,0 +1,154 @@ +[%# 1.0@bugzilla.org %] +[%# The contents of this file are subject to the Mozilla Public + # License Version 1.1 (the "License"); you may not use this file + # except in compliance with the License. You may obtain a copy of + # the License at http://www.mozilla.org/MPL/ + # + # Software distributed under the License is distributed on an "AS + # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or + # implied. See the License for the specific language governing + # rights and limitations under the License. + # + # The Original Code is the Bugzilla Bug Tracking System. + # + # Contributor(s): Marc Schumann <wurblzap@gmail.com> + #%] + +[%# INTERFACE: + # + # message: message tag specifying a global/messages.html.tmpl + # message + # listselectionvalues: selection values to recreate the current user list. + # editusers: is viewing user member of editusers? + # otheruser: Bugzilla::User object of viewed user. + # groups: array of group information (name, grant type, + # canbless) for viewed user. + #%] + +[% PROCESS global/header.html.tmpl + title = "Edit user $login" + message = message + style_urls = ['skins/standard/editusers.css'] +%] + +[% PROCESS admin/users/listselectvars.html.tmpl + listselectionvalues = listselectionvalues +%] + +<form method="post" action="editusers.cgi"> +<table class="main"> + [% PROCESS admin/users/userdata.html.tmpl + editform = 1 + editusers = editusers + otheruser = otheruser + %] + [% IF groups.size %] + <tr> + <th>Group access:</th> + <td> + <table class="groups"> + <tr> + [% IF editusers %] + <th colspan="3"> + Can turn these [% terms.bits %] on for other users + </th> + [% END %] + </tr> + <tr> + [% IF editusers %] + <td style="text-align: center; font-weight: bold">|</td> + [% END %] + <th colspan="2">User is a member of these groups</th> + </tr> + [% FOREACH group = groups %] + [% perms = permissions.${group.id} %] + <tr class="[% 'in' IF perms.regexpmember || perms.derivedmember %]direct"> + [% IF editusers %] + <td class="checkbox"> + [% '[' IF perms.indirectbless %] + [% %]<input type="checkbox" + name="bless_[% group.id %]" + value="1" + [% ' checked="checked"' IF perms.directbless %] /> + [% ']' IF perms.indirectbless %] + [% %]<input type="hidden" name="oldbless_[% group.id %]" + value="[% perms.directbless %]" /></td> + [% END %] + <td class="checkbox"> + [% '[' IF perms.derivedmember %] + [% '*' IF perms.regexpmember %] + [%%]<input type="checkbox" + id="group_[% group.id %]" + name="group_[% group.id %]" + value="1" + [% ' checked="checked"' IF perms.directmember %] /> + [% '*' IF perms.regexpmember %] + [% ']' IF perms.derivedmember %] + [% %]<input type="hidden" name="oldgroup_[% group.id %]" + value="[% perms.directmember %]" /></td> + <td class="groupname"> + <label for="group_[% group.id %]"> + <strong>[% group.name FILTER html %]:</strong> + [%+ group.description FILTER html %] + </label> + </td> + </tr> + [% END %] + </table> + </td> + </tr> + [% END %] +</table> + +<p> + <input type="submit" value="Update" /> + <input type="hidden" name="userid" value="[% otheruser.id %]" /> + <input type="hidden" name="action" value="update" /> + [% INCLUDE listselectionhiddenfields %] +</p> +</form> +<p> + User is a member of any groups shown with a check or grey bar. + A grey bar indicates indirect membership, either derived from other + groups (marked with square brackets) or via regular expression + (marked with '*'). +</p> +[% IF editusers %] + <p> + Square brackets around the bless checkbox indicate the ability + to bless users (grant them membership in the group) as a result + of membership in another group. + </p> +[% END %] + +[% IF Param('allowuserdeletion') && editusers %] + <form method="post" action="editusers.cgi"> + <p> + <input type="submit" value="Delete User" /> + <input type="hidden" name="action" value="del" /> + <input type="hidden" name="userid" value="[% otheruser.id %]" /> + [% INCLUDE listselectionhiddenfields %] + </p> + </form> +[% END %] + +<p> + You can also + [% IF editusers %] + <a href="editusers.cgi?action=add[% INCLUDE listselectionurlparams %]">add + a new user</a> + [% IF listselectionvalues %], + [% END %] + [% END %] + [% IF listselectionvalues %] + go + <a href="editusers.cgi?action=list[% INCLUDE listselectionurlparams %]">back + to the user list</a>, + [% END %] + [% IF editusers OR listselectionvalues %] + or + [% END %] + <a href="editusers.cgi">find other users</a>. +</p> + +[% PROCESS global/footer.html.tmpl %] diff --git a/template/en/default/admin/users/list.html.tmpl b/template/en/default/admin/users/list.html.tmpl new file mode 100644 index 000000000..9cbd7530d --- /dev/null +++ b/template/en/default/admin/users/list.html.tmpl @@ -0,0 +1,98 @@ +[%# 1.0@bugzilla.org %] +[%# The contents of this file are subject to the Mozilla Public + # License Version 1.1 (the "License"); you may not use this file + # except in compliance with the License. You may obtain a copy of + # the License at http://www.mozilla.org/MPL/ + # + # Software distributed under the License is distributed on an "AS + # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or + # implied. See the License for the specific language governing + # rights and limitations under the License. + # + # The Original Code is the Bugzilla Bug Tracking System. + # + # Contributor(s): Marc Schumann <wurblzap@gmail.com> + #%] + +[%# INTERFACE: + # + # listselectionvalues: selection values to recreate the current user list. + # editusers: is viewing user member of editusers? + # users: list of user information (id, login_name, realname, + # disabledtext). + #%] + +[% PROCESS global/header.html.tmpl + title = "Select user" + style_urls = ['skins/standard/editusers.css'] +%] + +[% PROCESS admin/users/listselectvars.html.tmpl + listselectionvalues = listselectionvalues +%] + +[% listselectionurlparams = INCLUDE listselectionurlparams %] + +[% columns = + [{name => 'login_name' + heading => 'Edit user...' + contentlink => 'editusers.cgi?action=edit&userid=%%userid%%' _ + listselectionurlparams + allow_html_content => 1 + } + {name => 'realname' + heading => 'Real name' + allow_html_content => 1 + } + ] +%] + +[% IF Param('allowuserdeletion') && editusers %] + [% columns.push({heading => 'Action' + content => 'Delete' + contentlink => 'editusers.cgi?action=del' _ + '&userid=%%userid%%' _ + listselectionurlparams + } + ) + %] +[% END %] + +[% FOREACH thisuser = users %] + [%# We FILTER html here because we need admin/table.html.tmpl to accept HTML + # for styling, so we cannot let admin/table.html.tmpl do the FILTER. + #%] + [% thisuser.login_name = BLOCK %] + [% thisuser.login_name FILTER html %] + [% END %] + [% IF thisuser.realname %] + [% thisuser.realname = BLOCK %] + [% thisuser.realname FILTER html %] + [% END %] + [% ELSE %] + [% SET thisuser.realname = '<span style="color: red">missing</span>' %] + [% END %] + [% IF thisuser.disabledtext %] + [% thisuser.login_name = "<span class=\"bz_inactive\">$thisuser.login_name</span>" %] + [% thisuser.realname = "<span class=\"bz_inactive\">$thisuser.realname</span>" %] + [% END %] +[% END %] + +<p>[% users.size %] user[% "s" UNLESS users.size == 1 %] found.</p> + +[% PROCESS admin/table.html.tmpl + columns = columns + data = users +%] + +<p> + If you do not wish to modify a user account at this time, you can + <a href="editusers.cgi">find other users</a> + [% IF editusers %] + or + <a href="editusers.cgi?action=add[% INCLUDE listselectionurlparams %]">add + a new user</a> + [% END %]. +</p> + +[% PROCESS global/footer.html.tmpl %] diff --git a/template/en/default/admin/users/listselectvars.html.tmpl b/template/en/default/admin/users/listselectvars.html.tmpl new file mode 100644 index 000000000..781e85a7d --- /dev/null +++ b/template/en/default/admin/users/listselectvars.html.tmpl @@ -0,0 +1,34 @@ +[%# 1.0@bugzilla.org %] +[%# The contents of this file are subject to the Mozilla Public + # License Version 1.1 (the "License"); you may not use this file + # except in compliance with the License. You may obtain a copy of + # the License at http://www.mozilla.org/MPL/ + # + # Software distributed under the License is distributed on an "AS + # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or + # implied. See the License for the specific language governing + # rights and limitations under the License. + # + # The Original Code is the Bugzilla Bug Tracking System. + # + # Contributor(s): Marc Schumann <wurblzap@gmail.com> + #%] + +[%# INTERFACE: + # + # listselectionvalues: selection values to recreate the current user list. + #%] + +[% BLOCK listselectionurlparams %] + [% FOREACH field = listselectionvalues.keys %]& + [% field FILTER url_quote %]= + [% listselectionvalues.$field FILTER url_quote %] + [% END %] +[% END %] + +[% BLOCK listselectionhiddenfields %] + [% FOREACH field = listselectionvalues.keys %] + <input type="hidden" name="[% field FILTER html %]" + value="[% listselectionvalues.$field FILTER html %]" /> + [% END %] +[% END %] diff --git a/template/en/default/admin/users/search.html.tmpl b/template/en/default/admin/users/search.html.tmpl new file mode 100644 index 000000000..748670a68 --- /dev/null +++ b/template/en/default/admin/users/search.html.tmpl @@ -0,0 +1,70 @@ +[%# 1.0@bugzilla.org %] +[%# The contents of this file are subject to the Mozilla Public + # License Version 1.1 (the "License"); you may not use this file + # except in compliance with the License. You may obtain a copy of + # the License at http://www.mozilla.org/MPL/ + # + # Software distributed under the License is distributed on an "AS + # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or + # implied. See the License for the specific language governing + # rights and limitations under the License. + # + # The Original Code is the Bugzilla Bug Tracking System. + # + # Contributor(s): Marc Schumann <wurblzap@gmail.com> + #%] + +[%# INTERFACE: + # + # editusers: is viewing user member of editusers? + # restrictablegroups: list of groups visible to the user: + # id: group id + # name: group name + #%] + + +[% PROCESS global/header.html.tmpl + title = "Search users" + style_urls = ['skins/standard/editusers.css'] + onload = "document.forms['f'].matchstr.focus()" +%] + +[% PROCESS admin/users/listselectvars.html.tmpl + listselectionvalues = listselectionvalues +%] + +<form name="f" method="get" action="editusers.cgi"> +<input type="hidden" name="action" value="list" /> +<p><label for="matchstr">List users with login name matching</label> +<input size="32" name="matchstr" id="matchstr" /> +<select name="matchtype"> + <option value="substr" selected="selected">case-insensitive substring</option> + <option value="regexp">case-insensitive regexp</option> + <option value="notregexp">not (case-insensitive regexp)</option> +</select> +<input type="submit" value="Search" /></p> + +[% IF restrictablegroups.size %] + <p><input type="checkbox" name="grouprestrict" value="1" id="grouprestrict" /> + <label for="grouprestrict">Restrict to users belonging to group</label> + <select name="groupid" + onchange="document.forms['f'].grouprestrict.checked=true"> + [% FOREACH group = restrictablegroups %] + <option value="[% group.id FILTER html %]">[% group.name FILTER html %]</option> + [% END %] + </select></p> +[% END %] +</form> + +[% IF editusers %] + <p> + You can also <a href="editusers.cgi?action=add">add a new user</a> + [%- IF listselectionvalues %], + or + <a href="editusers.cgi?action=list[% INCLUDE listselectionurlparams %]">show + the user list again</a> + [%- END %]. + </p> +[% END %] + +[% PROCESS global/footer.html.tmpl %] diff --git a/template/en/default/admin/users/userdata.html.tmpl b/template/en/default/admin/users/userdata.html.tmpl new file mode 100644 index 000000000..43ee627f1 --- /dev/null +++ b/template/en/default/admin/users/userdata.html.tmpl @@ -0,0 +1,79 @@ +[%# 1.0@bugzilla.org %] +[%# The contents of this file are subject to the Mozilla Public + # License Version 1.1 (the "License"); you may not use this file + # except in compliance with the License. You may obtain a copy of + # the License at http://www.mozilla.org/MPL/ + # + # Software distributed under the License is distributed on an "AS + # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or + # implied. See the License for the specific language governing + # rights and limitations under the License. + # + # The Original Code is the Bugzilla Bug Tracking System. + # + # Contributor(s): Marc Schumann <wurblzap@gmail.com> + #%] + +[%# INTERFACE: + # + # editform: is this an edit form? (It's a create form otherwise) + # editusers: is viewing user member of editusers? + # otheruser: Bugzilla::User object of user to edit + #%] + +<tr> + <th><label for="login">Login name:</label></th> + <td> + [% IF editusers %] + <input size="64" maxlength="255" name="login" + id="login" value="[% otheruser.login FILTER html %]" /> + [% IF editform %] + <input type="hidden" name="loginold" + value="[% otheruser.login FILTER html %]" /> + [% END %] + [% ELSE %] + [% otheruser.login FILTER html %] + [% END %] + </td> +</tr> +<tr> + <th><label for="name">Real name:</label></th> + <td> + [% IF editusers %] + <input size="64" maxlength="255" name="name" + id="name" value="[% otheruser.name FILTER html %]" /> + [% IF editform %] + <input type="hidden" name="nameold" + value="[% otheruser.name FILTER html %]" /> + [% END %] + [% ELSE %] + [% otheruser.name FILTER html %] + [% END %] + </td> +</tr> +[% IF editusers %] + <tr> + <th><label for="password">Password:</label></th> + <td> + <input type="password" size="16" maxlength="16" name="password" + id="password" value="" /> + [% IF editform %]<br /> + (Enter new password to change.) + [% END %] + </td> + </tr> + <tr> + <th><label for="disabledtext">Disable text:</label></th> + <td> + <textarea name="disabledtext" rows="10" + id="disabledtext" + cols="60">[% otheruser.disabledtext FILTER html %]</textarea><br /> + (If non-empty, then the account will be disabled, and this text should + explain why.) + [% IF editform %] + <input type="hidden" name="disabledtextold" + value="[% otheruser.disabledtext FILTER html %]" /> + [% END %] + </td> + </tr> +[% END %] diff --git a/template/en/default/filterexceptions.pl b/template/en/default/filterexceptions.pl index 94a4168e2..bcbbcc8a5 100644 --- a/template/en/default/filterexceptions.pl +++ b/template/en/default/filterexceptions.pl @@ -581,6 +581,31 @@ 'deleted_bug_count' ], +'admin/users/confirm-delete.html.tmpl' => [ + 'andstring', + 'responsibilityterms.$responsibility', + 'bugs', + 'cc', + 'flags.requestee', + 'flags.setter', + 'longdescs', + 'namedqueries', + 'votes', + 'series', + 'watch.watched', + 'watch.watcher', + 'whine_events', + 'whine_schedules', + 'otheruser.id' +], + +'admin/users/edit.html.tmpl' => [ + 'otheruser.id', + 'group.id', + 'perms.directbless', + 'perms.directmember', +], + 'admin/components/edit.html.tmpl' => [ 'bug_count' ], diff --git a/template/en/default/global/code-error.html.tmpl b/template/en/default/global/code-error.html.tmpl index 3a2a9606e..259be667c 100644 --- a/template/en/default/global/code-error.html.tmpl +++ b/template/en/default/global/code-error.html.tmpl @@ -185,7 +185,11 @@ [% ELSIF error == "invalid_keyword_id" %] The keyword ID <em>[% id FILTER html %]</em> couldn't be found. - + + [% ELSIF error == "invalid_user_id" %] + [% title = "Invalid User ID" %] + There is no user account with ID <em>[% userid FILTER html %]</em>. + [% ELSIF error == "missing_bug_id" %] No [% terms.bug %] ID was given. diff --git a/template/en/default/global/messages.html.tmpl b/template/en/default/global/messages.html.tmpl index ba1476300..5ed2eecf8 100644 --- a/template/en/default/global/messages.html.tmpl +++ b/template/en/default/global/messages.html.tmpl @@ -30,7 +30,77 @@ [% message_tag = message %] [% message = BLOCK %] - [% IF message_tag == "buglist_adding_field" %] + [% IF message_tag == "account_created" %] + [% title = "User $otheruser.login created" %] + A new user account [% otheruser.login FILTER html %] has been created + successfully. + [% IF groups.size %] + You may want to edit the group settings now, using the form below. + [% END %] + + [% ELSIF message_tag == "account_updated" %] + [% IF changed_fields.size + + groups_added_to.size + groups_removed_from.size + + groups_granted_rights_to_bless.size + groups_denied_rights_to_bless.size %] + [% title = "User $loginold updated" %] + The following changes have been made to the user account + [%+ loginold FILTER html %]: + <ul> + [% FOREACH field = changed_fields %] + <li> + [% IF field == 'login_name' %] + The login is now [% otheruser.login FILTER html %]. + [% ELSIF field == 'realname' %] + The real name has been updated. + [% ELSIF field == 'cryptpassword' %] + A new password has been set. + [% ELSIF field == 'disabledtext' %] + The disable text has been modified. + [% END %] + </li> + [% END %] + [% IF groups_added_to.size %] + <li> + The account has been added to the + [%+ groups_added_to.join(', ') FILTER html %] + group[% 's' IF groups_added_to.size > 1 %]. + </li> + [% END %] + [% IF groups_removed_from.size %] + <li> + The account has been removed from the + [%+ groups_removed_from.join(', ') FILTER html %] + group[% 's' IF groups_removed_from.size > 1 %]. + </li> + [% END %] + [% IF groups_granted_rights_to_bless.size %] + <li> + The account has been granted rights to bless the + [%+ groups_granted_rights_to_bless.join(', ') FILTER html %] + group[% 's' IF groups_granted_rights_to_bless.size > 1 %]. + </li> + [% END %] + [% IF groups_denied_rights_to_bless.size %] + <li> + The account has been denied rights to bless the + [%+ groups_denied_rights_to_bless.join(', ') FILTER html %] + group[% 's' IF groups_denied_rights_to_bless.size > 1 %]. + </li> + [% END %] + </ul> + [% ELSE %] + [% title = "User $otheruser.login not changed" %] + You didn't request any change on the user account + [%+ otheruser.login FILTER html %]. + [% END %] + [%# changed_fields.join(', ') %] + + [% ELSIF message_tag == "account_deleted" %] + [% title = "User $otheruser.login deleted" %] + The user account [% otheruser.login FILTER html %] has been deleted + successfully. + + [% ELSIF message_tag == "buglist_adding_field" %] [% title = "Adding field to search page..." %] [% link = "Click here if the page does not redisplay automatically." %] diff --git a/template/en/default/global/user-error.html.tmpl b/template/en/default/global/user-error.html.tmpl index e0d43b5b1..eba6e97c6 100644 --- a/template/en/default/global/user-error.html.tmpl +++ b/template/en/default/global/user-error.html.tmpl @@ -108,6 +108,8 @@ [% IF group %] and [% END %] [% IF reason == "cant_bless" %] you don't have permissions to put people in or out of any group, + [% ELSIF reason == "not_visible" %] + there are visibility restrictions on certain user groups, [% END %] [% END %] @@ -146,6 +148,8 @@ products [% ELSIF object == "reports" %] whine reports + [% ELSIF object == "user" %] + the user you specified [% ELSIF object == "users" %] users [% ELSIF object == "versions" %] @@ -1046,6 +1050,16 @@ [% title = "Deletion not activated" %] Sorry, the deletion of user accounts is not allowed. + [% ELSIF error == "user_has_responsibility" %] + [% title = "Can't Delete User Account" %] + The user you want to delete is set up for roles as initial [% terms.bug %] + owner or QA contact for at least one component. + For this reason, you cannot delete the account at this time. + + [% ELSIF error == "user_login_required" %] + [% title = "Login Name Required" %] + You must enter a login name for the new user. + [% ELSIF error == "votes_must_be_nonnegative" %] [% title = "Votes Must Be Non-negative" %] Only use non-negative numbers for your [% terms.bug %] votes. |