From 43b7dc314234e476a80d9acbd07292d7286cca5a Mon Sep 17 00:00:00 2001 From: "lpsolit%gmail.com" <> Date: Wed, 2 Apr 2008 22:42:25 +0000 Subject: Bug 405946: Some emails are not sent in the language chosen by the addressee - Patch by Frédéric Buclin r=wurblzap a=LpSolit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Bugzilla/Bug.pm | 24 ++----- Bugzilla/Flag.pm | 58 ++++++++-------- Bugzilla/Token.pm | 83 ++++++++++++----------- editproducts.cgi | 11 +-- process_bug.cgi | 4 +- relogin.cgi | 8 +-- sanitycheck.cgi | 10 ++- template/en/default/account/cancel-token.txt.tmpl | 2 +- template/en/default/email/votes-removed.txt.tmpl | 23 ++++++- template/en/default/global/user-error.html.tmpl | 5 -- token.cgi | 18 ++--- userprefs.cgi | 2 +- whine.pl | 8 ++- whineatnews.pl | 6 +- 14 files changed, 129 insertions(+), 133 deletions(-) diff --git a/Bugzilla/Bug.pm b/Bugzilla/Bug.pm index 3d4c4b869..e523db6ae 100755 --- a/Bugzilla/Bug.pm +++ b/Bugzilla/Bug.pm @@ -3024,7 +3024,6 @@ sub RemoveVotes { if (scalar(@list)) { foreach my $ref (@list) { my ($name, $userid, $oldvotes, $votesperuser, $maxvotesperbug) = (@$ref); - my $s; $maxvotesperbug = min($votesperuser, $maxvotesperbug); @@ -3038,23 +3037,13 @@ sub RemoveVotes { my $removedvotes = $oldvotes - $newvotes; - $s = ($oldvotes == 1) ? "" : "s"; - my $oldvotestext = "You had $oldvotes vote$s on this bug."; - - $s = ($removedvotes == 1) ? "" : "s"; - my $removedvotestext = "You had $removedvotes vote$s removed from this bug."; - - my $newvotestext; if ($newvotes) { $dbh->do("UPDATE votes SET vote_count = ? " . "WHERE bug_id = ? AND who = ?", undef, ($newvotes, $id, $userid)); - $s = $newvotes == 1 ? "" : "s"; - $newvotestext = "You still have $newvotes vote$s on this bug." } else { $dbh->do("DELETE FROM votes WHERE bug_id = ? AND who = ?", undef, ($id, $userid)); - $newvotestext = "You have no more votes remaining on this bug."; } # Notice that we did not make sure that the user fit within the $votesperuser @@ -3065,7 +3054,6 @@ sub RemoveVotes { # Now lets send the e-mail to alert the user to the fact that their votes have # been reduced or removed. my $vars = { - 'to' => $name . Bugzilla->params->{'emailsuffix'}, 'bugid' => $id, 'reason' => $reason, @@ -3073,19 +3061,17 @@ sub RemoveVotes { 'votesremoved' => $removedvotes, 'votesold' => $oldvotes, 'votesnew' => $newvotes, - - 'votesremovedtext' => $removedvotestext, - 'votesoldtext' => $oldvotestext, - 'votesnewtext' => $newvotestext, - - 'count' => $removedvotes . "\n " . $newvotestext }; + my $voter = new Bugzilla::User($userid); + my $template = Bugzilla->template_inner($voter->settings->{'lang'}->{'value'}); + my $msg; - my $template = Bugzilla->template; $template->process("email/votes-removed.txt.tmpl", $vars, \$msg); push(@messages, $msg); } + Bugzilla->template_inner(""); + my $votes = $dbh->selectrow_array("SELECT SUM(vote_count) " . "FROM votes WHERE bug_id = ?", undef, $id) || 0; diff --git a/Bugzilla/Flag.pm b/Bugzilla/Flag.pm index f8c43b508..a65a8268b 100644 --- a/Bugzilla/Flag.pm +++ b/Bugzilla/Flag.pm @@ -1063,56 +1063,54 @@ or deleted. sub notify { my ($flag, $bug, $attachment) = @_; - my $template = Bugzilla->template; - # There is nobody to notify. return unless ($flag->{'addressee'} || $flag->type->cc_list); - my $attachment_is_private = $attachment ? $attachment->isprivate : undef; - # If the target bug is restricted to one or more groups, then we need # to make sure we don't send email about it to unauthorized users # on the request type's CC: list, so we have to trawl the list for users # not in those groups or email addresses that don't have an account. my @bug_in_groups = grep {$_->{'ison'} || $_->{'mandatory'}} @{$bug->groups}; + my $attachment_is_private = $attachment ? $attachment->isprivate : undef; - if (scalar(@bug_in_groups) || $attachment_is_private) { - my @new_cc_list; - foreach my $cc (split(/[, ]+/, $flag->type->cc_list)) { - my $ccuser = new Bugzilla::User({ name => $cc }) || next; - - next if (scalar(@bug_in_groups) && !$ccuser->can_see_bug($bug->bug_id)); - next if $attachment_is_private - && Bugzilla->params->{"insidergroup"} - && !$ccuser->in_group(Bugzilla->params->{"insidergroup"}); - push(@new_cc_list, $cc); - } - $flag->type->{'cc_list'} = join(", ", @new_cc_list); + my %recipients; + foreach my $cc (split(/[, ]+/, $flag->type->cc_list)) { + my $ccuser = new Bugzilla::User({ name => $cc }); + next if (scalar(@bug_in_groups) && (!$ccuser || !$ccuser->can_see_bug($bug->bug_id))); + next if $attachment_is_private && (!$ccuser || !$ccuser->is_insider); + # Prevent duplicated entries due to case sensitivity. + $cc = $ccuser ? $ccuser->email : $cc; + $recipients{$cc} = $ccuser; } - # If there is nobody left to notify, return. - return unless ($flag->{'addressee'} || $flag->type->cc_list); - - my @recipients = split(/[, ]+/, $flag->type->cc_list); # Only notify if the addressee is allowed to receive the email. if ($flag->{'addressee'} && $flag->{'addressee'}->email_enabled) { - push @recipients, $flag->{'addressee'}->email; + $recipients{$flag->{'addressee'}->email} = $flag->{'addressee'}; } - # Process and send notification for each recipient - foreach my $to (@recipients) - { - next unless $to; + # Process and send notification for each recipient. + # If there are users in the CC list who don't have an account, + # use the default language for email notifications. + my $default_lang; + if (grep { !$_ } values %recipients) { + my $default_user = new Bugzilla::User(); + $default_lang = $default_user->settings->{'lang'}->{'value'}; + } + + foreach my $to (keys %recipients) { my $vars = { 'flag' => $flag, 'to' => $to, 'bug' => $bug, 'attachment' => $attachment}; + + my $lang = $recipients{$to} ? + $recipients{$to}->settings->{'lang'}->{'value'} : $default_lang; + + my $template = Bugzilla->template_inner($lang); my $message; - my $rv = $template->process("request/email.txt.tmpl", $vars, \$message); - if (!$rv) { - Bugzilla->cgi->header(); - ThrowTemplateError($template->error()); - } + $template->process("request/email.txt.tmpl", $vars, \$message) + || ThrowTemplateError($template->error()); + Bugzilla->template_inner(""); MessageToMTA($message); } } diff --git a/Bugzilla/Token.pm b/Bugzilla/Token.pm index 2f911fca1..157cc0622 100644 --- a/Bugzilla/Token.pm +++ b/Bugzilla/Token.pm @@ -34,6 +34,7 @@ use Bugzilla::Constants; use Bugzilla::Error; use Bugzilla::Mailer; use Bugzilla::Util; +use Bugzilla::User; use Date::Format; use Date::Parse; @@ -80,20 +81,24 @@ sub issue_new_user_account_token { $template->process('account/email/request-new.txt.tmpl', $vars, \$message) || ThrowTemplateError($template->error()); + # In 99% of cases, the user getting the confirmation email is the same one + # who made the request, and so it is reasonable to send the email in the same + # language used to view the "Create a New Account" page (we cannot use his + # user prefs as the user has no account yet!). MessageToMTA($message); } sub IssueEmailChangeToken { - my ($userid, $old_email, $new_email) = @_; + my ($user, $old_email, $new_email) = @_; my $email_suffix = Bugzilla->params->{'emailsuffix'}; - my ($token, $token_ts) = _create_token($userid, 'emailold', $old_email . ":" . $new_email); + my ($token, $token_ts) = _create_token($user->id, 'emailold', $old_email . ":" . $new_email); - my $newtoken = _create_token($userid, 'emailnew', $old_email . ":" . $new_email); + my $newtoken = _create_token($user->id, 'emailnew', $old_email . ":" . $new_email); # Mail the user the token along with instructions for using it. - my $template = Bugzilla->template; + my $template = Bugzilla->template_inner($user->settings->{'lang'}->{'value'}); my $vars = {}; $vars->{'oldemailaddress'} = $old_email . $email_suffix; @@ -118,38 +123,34 @@ sub IssueEmailChangeToken { $template->process("account/email/change-new.txt.tmpl", $vars, \$message) || ThrowTemplateError($template->error()); + Bugzilla->template_inner(""); MessageToMTA($message); } # Generates a random token, adds it to the tokens table, and sends it # to the user with instructions for using it to change their password. sub IssuePasswordToken { - my $loginname = shift; + my $user = shift; my $dbh = Bugzilla->dbh; - my $template = Bugzilla->template; - my $vars = {}; - # Retrieve the user's ID from the database. - trick_taint($loginname); - my ($userid, $too_soon) = - $dbh->selectrow_array('SELECT profiles.userid, tokens.issuedate - FROM profiles - LEFT JOIN tokens - ON tokens.userid = profiles.userid - AND tokens.tokentype = ? - AND tokens.issuedate > NOW() - ' . - $dbh->sql_interval(10, 'MINUTE') . ' - WHERE ' . $dbh->sql_istrcmp('login_name', '?'), - undef, ('password', $loginname)); + my $too_soon = + $dbh->selectrow_array('SELECT 1 FROM tokens + WHERE userid = ? + AND tokentype = ? + AND issuedate > NOW() - ' . + $dbh->sql_interval(10, 'MINUTE'), + undef, ($user->id, 'password')); ThrowUserError('too_soon_for_new_token', {'type' => 'password'}) if $too_soon; - my ($token, $token_ts) = _create_token($userid, 'password', $::ENV{'REMOTE_ADDR'}); + my ($token, $token_ts) = _create_token($user->id, 'password', $::ENV{'REMOTE_ADDR'}); # Mail the user the token along with instructions for using it. - $vars->{'token'} = $token; - $vars->{'emailaddress'} = $loginname . Bugzilla->params->{'emailsuffix'}; + my $template = Bugzilla->template_inner($user->settings->{'lang'}->{'value'}); + my $vars = {}; + $vars->{'token'} = $token; + $vars->{'emailaddress'} = $user->email; $vars->{'max_token_age'} = MAX_TOKEN_AGE; $vars->{'token_ts'} = $token_ts; @@ -158,6 +159,7 @@ sub IssuePasswordToken { $vars, \$message) || ThrowTemplateError($template->error()); + Bugzilla->template_inner(""); MessageToMTA($message); } @@ -205,31 +207,28 @@ sub GenerateUniqueToken { return $token; } -# Cancels a previously issued token and notifies the system administrator. +# Cancels a previously issued token and notifies the user. # This should only happen when the user accidentally makes a token request # or when a malicious hacker makes a token request on behalf of a user. sub Cancel { my ($token, $cancelaction, $vars) = @_; my $dbh = Bugzilla->dbh; - my $template = Bugzilla->template; $vars ||= {}; # Get information about the token being canceled. trick_taint($token); - my ($issuedate, $tokentype, $eventdata, $loginname) = + my ($issuedate, $tokentype, $eventdata, $userid) = $dbh->selectrow_array('SELECT ' . $dbh->sql_date_format('issuedate') . ', - tokentype, eventdata, login_name + tokentype, eventdata, userid FROM tokens - LEFT JOIN profiles - ON tokens.userid = profiles.userid WHERE token = ?', undef, $token); - # If we are cancelling the creation of a new user account, then there + # If we are canceling the creation of a new user account, then there # is no entry in the 'profiles' table. - $loginname ||= $eventdata; - $vars->{'emailaddress'} = $loginname . Bugzilla->params->{'emailsuffix'}; - $vars->{'maintainer'} = Bugzilla->params->{'maintainer'}; + my $user = new Bugzilla::User($userid); + + $vars->{'emailaddress'} = $userid ? $user->email : $eventdata; $vars->{'remoteaddress'} = $::ENV{'REMOTE_ADDR'}; $vars->{'token'} = $token; $vars->{'tokentype'} = $tokentype; @@ -238,11 +237,13 @@ sub Cancel { $vars->{'cancelaction'} = $cancelaction; # Notify the user via email about the cancellation. + my $template = Bugzilla->template_inner($user->settings->{'lang'}->{'value'}); my $message; $template->process("account/cancel-token.txt.tmpl", $vars, \$message) || ThrowTemplateError($template->error()); + Bugzilla->template_inner(""); MessageToMTA($message); # Delete the token from the database. @@ -391,8 +392,8 @@ Bugzilla::Token - Provides different routines to manage tokens. use Bugzilla::Token; Bugzilla::Token::issue_new_user_account_token($login_name); - Bugzilla::Token::IssueEmailChangeToken($user_id, $old_email, $new_email); - Bugzilla::Token::IssuePasswordToken($login_name); + Bugzilla::Token::IssueEmailChangeToken($user, $old_email, $new_email); + Bugzilla::Token::IssuePasswordToken($user); Bugzilla::Token::DeletePasswordTokens($user_id, $reason); Bugzilla::Token::Cancel($token, $cancelaction, $vars); @@ -422,26 +423,26 @@ Bugzilla::Token - Provides different routines to manage tokens. Returns: Nothing. It throws an error if the same user made the same request in the last few minutes. -=item C +=item C Description: Sends two distinct tokens per email to the old and new email addresses to confirm the email address change for the given - user ID. These tokens remain valid for the next MAX_TOKEN_AGE days. + user. These tokens remain valid for the next MAX_TOKEN_AGE days. - Params: $user_id - The user ID of the user account requesting a new - email address. + Params: $user - User object of the user requesting a new + email address. $old_email - The current (old) email address of the user. $new_email - The new email address of the user. Returns: Nothing. -=item C +=item C - Description: Sends a token per email to the given login name. This token + Description: Sends a token per email to the given user. This token can be used to change the password (e.g. in case the user cannot remember his password and wishes to enter a new one). - Params: $login_name - The login name of the user requesting a new password. + Params: $user - User object of the user requesting a new password. Returns: Nothing. It throws an error if the same user made the same request in the last few minutes. diff --git a/editproducts.cgi b/editproducts.cgi index d168360a6..4347ce836 100755 --- a/editproducts.cgi +++ b/editproducts.cgi @@ -939,10 +939,7 @@ if ($action eq 'update') { my ($who, $id) = (@$vote); # If some votes are removed, RemoveVotes() returns a list # of messages to send to voters. - my $msgs = - RemoveVotes($id, $who, "The rules for voting on this product " . - "has changed;\nyou had too many votes " . - "for a single bug."); + my $msgs = RemoveVotes($id, $who, 'votes_too_many_per_bug'); foreach my $msg (@$msgs) { MessageToMTA($msg); } @@ -991,11 +988,7 @@ if ($action eq 'update') { foreach my $bug_id (@$bug_ids) { # RemoveVotes() returns a list of messages to send # in case some voters had too many votes. - my $msgs = - RemoveVotes($bug_id, $who, "The rules for voting on this " . - "product has changed; you had " . - "too many\ntotal votes, so all " . - "votes have been removed."); + my $msgs = RemoveVotes($bug_id, $who, 'votes_too_many_per_user'); foreach my $msg (@$msgs) { MessageToMTA($msg); } diff --git a/process_bug.cgi b/process_bug.cgi index ffa06cee3..97cd3098d 100755 --- a/process_bug.cgi +++ b/process_bug.cgi @@ -560,9 +560,7 @@ foreach my $bug (@bug_objects) { # If some votes have been removed, RemoveVotes() returns # a list of messages to send to voters. # We delay the sending of these messages till tables are unlocked. - $msgs = RemoveVotes($bug->id, 0, - "This bug has been moved to a different product"); - + $msgs = RemoveVotes($bug->id, 0, 'votes_bug_moved'); CheckIfVotedConfirmed($bug->id, Bugzilla->user->id); } diff --git a/relogin.cgi b/relogin.cgi index e2182699a..9d30d7c11 100755 --- a/relogin.cgi +++ b/relogin.cgi @@ -158,11 +158,11 @@ elsif ($action eq 'begin-sudo') { # Go ahead and send out the message now my $message; - $template->process('email/sudo.txt.tmpl', - { reason => $reason }, - \$message); + my $mail_template = Bugzilla->template_inner($target_user->settings->{'lang'}->{'value'}); + $mail_template->process('email/sudo.txt.tmpl', { reason => $reason }, \$message); + Bugzilla->template_inner(""); MessageToMTA($message); - + $vars->{'message'} = 'sudo_started'; $vars->{'target'} = $target_user->login; $target = 'global/message.html.tmpl'; diff --git a/sanitycheck.cgi b/sanitycheck.cgi index 57dca0c31..9aa5e345b 100755 --- a/sanitycheck.cgi +++ b/sanitycheck.cgi @@ -69,7 +69,15 @@ my $user = Bugzilla->login(LOGIN_REQUIRED); my $cgi = Bugzilla->cgi; my $dbh = Bugzilla->dbh; -my $template = Bugzilla->template; +# If the result of the sanity check is sent per email, then we have to +# take the user prefs into account rather than querying the web browser. +my $template; +if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) { + $template = Bugzilla->template_inner($user->settings->{'lang'}->{'value'}); +} +else { + $template = Bugzilla->template; +} my $vars = {}; print $cgi->header() unless Bugzilla->usage_mode == USAGE_MODE_CMDLINE; diff --git a/template/en/default/account/cancel-token.txt.tmpl b/template/en/default/account/cancel-token.txt.tmpl index 153c92e34..155c44136 100644 --- a/template/en/default/account/cancel-token.txt.tmpl +++ b/template/en/default/account/cancel-token.txt.tmpl @@ -32,7 +32,7 @@ If you did not request this, it could be either an honest mistake or someone attempting to break into your [% terms.Bugzilla %] account. Take a look at the information below and forward this email -to [% maintainer %] if you suspect foul play. +to [% Param('maintainer') %] if you suspect foul play. Token: [% token %] Token Type: [% tokentype %] diff --git a/template/en/default/email/votes-removed.txt.tmpl b/template/en/default/email/votes-removed.txt.tmpl index 8d8f79057..bfb37c90d 100644 --- a/template/en/default/email/votes-removed.txt.tmpl +++ b/template/en/default/email/votes-removed.txt.tmpl @@ -27,11 +27,28 @@ X-Bugzilla-Type: voteremoved Some or all of your votes have been removed from [% terms.bug %] [%+ bugid %]. -[% votesoldtext %] +You had [% votesold FILTER html %] [%+ IF votesold == 1 %]vote[% ELSE %]votes[% END +%] on this [% terms.bug %], but [% votesremoved FILTER html %] have been removed. -[% votesnewtext %] +[% IF votesnew %] +You still have [% votesnew FILTER html %] [%+ IF votesnew == 1 %]vote[% ELSE %]votes[% END %] on this [% terms.bug %]. +[% ELSE %] +You have no more votes remaining on this [% terms.bug %]. +[% END %] + +Reason: +[% IF reason == "votes_bug_moved" %] + This [% terms.bug %] has been moved to a different product. + +[% ELSIF reason == "votes_too_many_per_bug" %] + The rules for voting on this product has changed; + you had too many votes for a single [% terms.bug %]. + +[% ELSIF reason == "votes_too_many_per_user" %] + The rules for voting on this product has changed; you had + too many total votes, so all votes have been removed. +[% END %] -Reason: [% reason %] [% urlbase %]show_bug.cgi?id=[% bugid %] diff --git a/template/en/default/global/user-error.html.tmpl b/template/en/default/global/user-error.html.tmpl index 58a1cfcf6..e61f3a9ee 100644 --- a/template/en/default/global/user-error.html.tmpl +++ b/template/en/default/global/user-error.html.tmpl @@ -76,11 +76,6 @@ that login name. [% END %] - [% ELSIF error == "account_does_not_exist" %] - [% title = "Account Does Not Exist" %] - [% email FILTER html %] is not a valid [% terms.Bugzilla %] - account. - [% ELSIF error == "alias_has_comma_or_space" %] [% title = "Invalid Characters In Alias" %] The alias you entered, [% alias FILTER html %], diff --git a/token.cgi b/token.cgi index cf6b37811..c91c2f94f 100755 --- a/token.cgi +++ b/token.cgi @@ -101,11 +101,10 @@ if ($cgi->param('t')) { # If the user is requesting a password change, make sure they submitted # their login name and it exists in the database, and that the DB module is in # the list of allowed verification methods. -my $login_name; +my $user_account; if ( $::action eq 'reqpw' ) { - $login_name = $cgi->param('loginname'); - defined $login_name - || ThrowUserError("login_needed_for_password_change"); + my $login_name = $cgi->param('loginname') + || ThrowUserError("login_needed_for_password_change"); # check verification methods unless (Bugzilla->user->authorizer->can_change_password) { @@ -115,10 +114,7 @@ if ( $::action eq 'reqpw' ) { validate_email_syntax($login_name) || ThrowUserError('illegal_email_address', {addr => $login_name}); - my ($user_id) = $dbh->selectrow_array('SELECT userid FROM profiles WHERE ' . - $dbh->sql_istrcmp('login_name', '?'), - undef, $login_name); - $user_id || ThrowUserError("account_does_not_exist", {'email' => $login_name}); + $user_account = Bugzilla::User->check($login_name); } # If the user is changing their password, make sure they submitted a new @@ -142,7 +138,7 @@ if ( $::action eq 'chgpw' ) { # that variable and runs the appropriate code. if ($::action eq 'reqpw') { - requestChangePassword($login_name); + requestChangePassword($user_account); } elsif ($::action eq 'cfmpw') { confirmChangePassword(); } elsif ($::action eq 'cxlpw') { @@ -175,8 +171,8 @@ exit; ################################################################################ sub requestChangePassword { - my ($login_name) = @_; - Bugzilla::Token::IssuePasswordToken($login_name); + my ($user) = @_; + Bugzilla::Token::IssuePasswordToken($user); $vars->{'message'} = "password_change_request"; diff --git a/userprefs.cgi b/userprefs.cgi index 3880b9c38..b281fd214 100755 --- a/userprefs.cgi +++ b/userprefs.cgi @@ -138,7 +138,7 @@ sub SaveAccount { is_available_username($new_login_name) || ThrowUserError("account_exists", {email => $new_login_name}); - Bugzilla::Token::IssueEmailChangeToken($user->id, $old_login_name, + Bugzilla::Token::IssueEmailChangeToken($user, $old_login_name, $new_login_name); $vars->{'email_changes_saved'} = 1; diff --git a/whine.pl b/whine.pl index 8f4671321..e49e4816a 100755 --- a/whine.pl +++ b/whine.pl @@ -353,10 +353,11 @@ while (my $event = get_next_event) { # sub mail { my $args = shift; + my $addressee = $args->{recipient}; + # Don't send mail to someone whose bugmail notification is disabled. + return if $addressee->email_disabled; - # Don't send mail to someone on the nomail list. - return if $args->{recipient}->email_disabled; - + my $template = Bugzilla->template_inner($addressee->settings->{'lang'}->{'value'}); my $msg = ''; # it's a temporary variable to hold the template output $args->{'alternatives'} ||= []; @@ -387,6 +388,7 @@ sub mail { $template->process("whine/multipart-mime.txt.tmpl", $args, \$msg) or die($template->error()); + Bugzilla->template_inner(""); MessageToMTA($msg); delete $args->{'boundary'}; diff --git a/whineatnews.pl b/whineatnews.pl index 5c1ec655d..df9006230 100755 --- a/whineatnews.pl +++ b/whineatnews.pl @@ -85,9 +85,11 @@ foreach my $email (sort (keys %bugs)) { $vars->{'bugs'} = \@bugs; my $msg; - my $template = Bugzilla->template; - $template->process("email/whine.txt.tmpl", $vars, \$msg); + my $template = Bugzilla->template_inner($user->settings->{'lang'}->{'value'}); + $template->process("email/whine.txt.tmpl", $vars, \$msg) + or die($template->error()); + Bugzilla->template_inner(""); MessageToMTA($msg); print "$email " . join(" ", @{$bugs{$email}}) . "\n"; -- cgit v1.2.3-24-g4f1b