diff --git a/Bugzilla/Config/Donation.pm b/Bugzilla/Config/Donation.pm new file mode 100644 index 0000000000..cb674940da --- /dev/null +++ b/Bugzilla/Config/Donation.pm @@ -0,0 +1,36 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# This Source Code Form is "Incompatible With Secondary Licenses", as +# defined by the Mozilla Public License, v. 2.0. + +package Bugzilla::Config::Donation; + +use 5.10.1; +use strict; +use warnings; + +use Bugzilla::Config::Common; + +our $sortkey = 175; + +use constant get_param_list => ( + { + name => 'donation_banner_visibility', + type => 's', + choices => ['admins_only', 'end_users', 'disabled'], + default => 'admins_only', + checker => \&check_multi, + }, +); + +1; + +__END__ + +=head1 NAME + +Bugzilla::Config::Donation - Donation banner settings + +=cut diff --git a/Bugzilla/Donation.pm b/Bugzilla/Donation.pm new file mode 100644 index 0000000000..6e5d966acc --- /dev/null +++ b/Bugzilla/Donation.pm @@ -0,0 +1,129 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# This Source Code Form is "Incompatible With Secondary Licenses", as +# defined by the Mozilla Public License, v. 2.0. + +package Bugzilla::Donation; + +use 5.10.1; +use strict; +use warnings; + +use Bugzilla::Constants; +use Bugzilla::Token qw(issue_hash_token); + +use DateTime; + +use constant BANNER_URL => 'https://bugzilla.org/donate'; +use constant BANNER_MESSAGES => ( + 'Help us make Bugzilla better more often.', + 'A small donation helps keep Bugzilla moving forward.', + 'Support the people who keep Bugzilla running.', + 'Even a little funding helps Bugzilla improve more quickly.', + 'If Bugzilla helps your team, consider helping Bugzilla too.', +); + +sub get_banner { + my $user = Bugzilla->user; + return undef if !$user->id; + + my $visibility = Bugzilla->params->{'donation_banner_visibility'} || 'admins_only'; + return undef if $visibility eq 'disabled'; + return undef if $visibility eq 'admins_only' && !$user->in_group('admin'); + + my $settings = $user->settings; + my $pref = $settings->{'donate_banner_pref'}->{'value'}; + my $last_version = $settings->{'donate_banner_last_version'}->{'value'} || '0.0'; + my $next_date = $settings->{'donate_banner_reminder_date'}->{'value'} || '1970-01-01'; + my $current_version = BUGZILLA_VERSION; + + my $show; + my $show_thanks = 0; + if ($pref eq 'next_upgrade') { + $show = ($last_version ne $current_version); + $show_thanks = $show + && $user->in_group('admin') + && $last_version ne '0' + && $last_version ne '0.0'; + } + elsif ($pref eq 'specific_date') { + my $today = DateTime->now(time_zone => Bugzilla->local_timezone)->ymd; + $show = ($next_date le $today); + } + else { + $show = 0; + } + + return undef if !$show; + + my @messages = BANNER_MESSAGES; + my $message = $messages[int(rand(@messages))]; + + my $data = { + url => BANNER_URL, + message => $message, + show_thanks => $show_thanks, + visibility => $visibility, + settings_link => 'editparams.cgi?section=donation#donation_banner_visibility_desc', + token => issue_hash_token(['donation_banner']), + }; + + if ($visibility eq 'admins_only') { + $data->{'visibility_note'} + = 'This message is only shown to logged-in users with admin privs.'; + } + elsif ($user->in_group('admin')) { + $data->{'visibility_note'} + = 'This message is visible to all logged-in users.'; + } + + return $data; +} + +sub set_banner_preference { + my ($action) = @_; + my $user = Bugzilla->user; + my $settings = $user->settings; + + my $pref_setting = $settings->{'donate_banner_pref'}; + my $date_setting = $settings->{'donate_banner_reminder_date'}; + my $version_setting = $settings->{'donate_banner_last_version'}; + my $current_version = BUGZILLA_VERSION; + + if ($action eq 'next_upgrade') { + $pref_setting->set('next_upgrade'); + $version_setting->set($current_version); + return 'index.cgi'; + } + if ($action eq 'week' || $action eq 'month') { + my $days = $action eq 'week' ? 7 : 30; + my $dt = DateTime->now(time_zone => Bugzilla->local_timezone); + $dt->add(days => $days); + $pref_setting->set('specific_date'); + $date_setting->set($dt->ymd); + $version_setting->set($current_version); + return 'index.cgi'; + } + if ($action eq 'never') { + $pref_setting->set('never'); + $version_setting->set($current_version); + return 'index.cgi'; + } + if ($action eq 'date') { + return 'userprefs.cgi?tab=donate'; + } + + return 'index.cgi'; +} + +1; + +__END__ + +=head1 NAME + +Bugzilla::Donation - Donation banner helpers + +=cut diff --git a/Bugzilla/Install.pm b/Bugzilla/Install.pm index c982c072ed..df41b331ce 100644 --- a/Bugzilla/Install.pm +++ b/Bugzilla/Install.pm @@ -58,6 +58,24 @@ sub SETTINGS { category => 'Searching' }, + # 2026-07-01 bugzilla@mozilla.org -- Bug 1983391 + { + name => 'donate_banner_pref', + options => ['next_upgrade', 'specific_date', 'never'], + default => 'next_upgrade', + category => 'Bugzilla.org', + }, + { + name => 'donate_banner_last_version', + default => '0.0', + category => 'Bugzilla.org', + }, + { + name => 'donate_banner_reminder_date', + default => '1970-01-01', + category => 'Bugzilla.org', + }, + # 2005-03-10 travis@sedsystems.ca -- Bug 199048 { name => 'comment_sort_order', diff --git a/Bugzilla/Install/DB.pm b/Bugzilla/Install/DB.pm index 0265b33507..2227ca1712 100644 --- a/Bugzilla/Install/DB.pm +++ b/Bugzilla/Install/DB.pm @@ -3076,7 +3076,13 @@ sub _rederive_regex_groups { $sth->execute(GRANT_REGEXP); while (my ($uid, $login, $gid, $rexp, $present) = $sth->fetchrow_array()) { - if ($login =~ m/$rexp/i) { + my $matches = eval { $login =~ m/$rexp/i ? 1 : 0 }; + if ($@) { + print "Skipping invalid group regexp for group $gid: $rexp\n"; + next; + } + + if ($matches) { $sth_add->execute($uid, $gid) unless $present; } else { diff --git a/index.cgi b/index.cgi index 82fa5ede97..8007af226d 100755 --- a/index.cgi +++ b/index.cgi @@ -15,6 +15,8 @@ use lib qw(. lib local/lib/perl5); use Bugzilla; use Bugzilla::Constants; use Bugzilla::Error; +use Bugzilla::Token; +use Bugzilla::Donation; use Bugzilla::Update; use Digest::MD5 qw(md5_hex); use List::MoreUtils qw(any); @@ -44,6 +46,19 @@ if ($cgi->param('logout')) { $cgi->delete('logout'); } +my $donate_action = $cgi->param('donate_action'); +if ($donate_action) { + Bugzilla->login(LOGIN_REQUIRED) unless Bugzilla->user->id; + + my $token = $cgi->param('token'); + check_hash_token($token, ['donation_banner']); + + my $redirect = Bugzilla::Donation::set_banner_preference($donate_action); + + print $cgi->redirect(-uri => $redirect); + exit; +} + # our weak etag is based on the Bugzilla version parameter (BMO customization) and the announcehtml # if either change, the cache will be considered invalid. my @etag_parts = ( @@ -90,6 +105,8 @@ else { $vars->{'release'} = Bugzilla::Update::get_notifications(); } + $vars->{'donation'} = Bugzilla::Donation::get_banner(); + # Generate and return the UI (HTML page) from the appropriate template. $template->process("index.html.tmpl", $vars) or ThrowTemplateError($template->error()); diff --git a/skins/standard/global.css b/skins/standard/global.css index 973cf85df7..6ce3802ab2 100644 --- a/skins/standard/global.css +++ b/skins/standard/global.css @@ -1050,6 +1050,148 @@ input[type="radio"]:checked { color: var(--secondary-label-color); } +.donation_banner { + border-color: rgba(var(--accent-color-green-1), 0.75); + color: var(--primary-text-color); + background: var(--primary-region-background-color); + box-shadow: var(--primary-region-box-shadow); + font-weight: normal; +} + +.donation_banner_layout { + display: flex; + align-items: center; + gap: 1rem; +} + +.donation_banner_image { + width: 110px; + height: 110px; + flex: 0 0 auto; + object-fit: contain; + object-position: center; + border: 1px dashed rgba(var(--accent-color-green-1), 0.7); + border-radius: 10px; + background: rgba(var(--accent-color-green-1), 0.08); +} + +.donation_banner_content { + flex: 1 1 auto; + padding-top: 0.35rem; +} + +.donation_banner_thanks { + margin: 0 0 0.5rem; + color: var(--primary-text-color); + font-weight: bold; +} + +.donation_banner_message { + margin: 0 0 0.6rem; + color: var(--primary-text-color); + font-size: 1.08rem; + line-height: 1.45; +} + +.donation_banner_cta_row { + margin: 0 0 0.75rem; +} + +.donation_banner_cta { + display: inline-block; + padding: 0.55rem 1rem; + border: 1px solid rgb(var(--accent-color-green-1)); + border-radius: 5px; + background: rgb(var(--accent-color-green-1)); + color: #fff !important; + text-decoration: none; + font-weight: bold; + font-size: 1rem; +} + +.donation_banner_cta:hover { + border-color: rgb(var(--accent-color-green-1)); + background: rgba(var(--accent-color-green-1), 0.9); + text-decoration: none; +} + +.donation_banner_form { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 0.5rem; +} + +.donation_banner_label { + font-weight: bold; +} + +.donation_banner_select { + min-width: 240px; +} + +.donation_banner_submit { + padding: 0.35rem 0.8rem; +} + +.donation_banner_submit:hover { + border-color: var(--hovered-secondary-button-border-color); + color: var(--hovered-secondary-button-foreground-color); + background: var(--hovered-secondary-button-background-color); +} + +.donation_banner_form_subtle { + font-size: 0.9rem; + opacity: 0.9; +} + +.donation_banner_form_subtle .donation_banner_label { + font-weight: normal; + color: var(--secondary-label-color); +} + +.donation_banner_form_subtle .donation_banner_select { + min-width: 230px; + padding: 0.25rem 0.4rem; + font-size: 0.9rem; +} + +.donation_banner_form_subtle .donation_banner_submit { + padding: 0.25rem 0.65rem; + font-size: 0.88rem; +} + +@media (max-width: 720px) { + .donation_banner_layout { + align-items: flex-start; + } + + .donation_banner_image { + width: 84px; + height: 84px; + } + + .donation_banner_select { + min-width: 100%; + } +} + +@media screen and (prefers-color-scheme: dark) { + .donation_banner { + border-color: rgb(var(--accent-color-lightgreen-1)); + background: rgba(var(--accent-color-lightgreen-1), 0.12); + } + + .donation_banner_image { + border-color: rgba(var(--accent-color-lightgreen-1), 0.55); + background: rgba(var(--accent-color-lightgreen-1), 0.12); + } + + .donation_banner_form_subtle { + opacity: 1; + } +} + /** * Global header */ diff --git a/skins/standard/index.css b/skins/standard/index.css index 07e67c2885..926201b81b 100644 --- a/skins/standard/index.css +++ b/skins/standard/index.css @@ -21,18 +21,29 @@ text-align: center; } - #new_release { - margin: 1em; - border: 2px solid rgb(var(--accent-color-red-1)); + /* Shared styling for home page banners (new release + donation). Each banner + sets its own border-color; the shared rule uses the border-width/-style + longhands so it never resets that color regardless of stylesheet order. */ + .home_banner { + box-sizing: border-box; + max-width: 640px; + margin: 1em auto; padding: 0.5em 1em; - font-weight: bold; + border-width: 2px; + border-style: solid; + border-radius: 8px; } - #new_release .notice { + .home_banner .notice { font-size: var(--font-size-small); font-weight: normal; } + #new_release { + border-color: rgb(var(--accent-color-red-1)); + font-weight: bold; + } + #welcome-admin a { font-weight: bold; } diff --git a/template/en/default/account/prefs/donate.html.tmpl b/template/en/default/account/prefs/donate.html.tmpl new file mode 100644 index 0000000000..b0f4416915 --- /dev/null +++ b/template/en/default/account/prefs/donate.html.tmpl @@ -0,0 +1,63 @@ +[%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. + # + # This Source Code Form is "Incompatible With Secondary Licenses", as + # defined by the Mozilla Public License, v. 2.0. + #%] + +[% + donate_pref = user.settings.donate_banner_pref.value; + reminder_date = user.settings.donate_banner_reminder_date.value; + # The setting defaults to the epoch as a "show immediately" sentinel; show + # today in the picker instead, since reminders are always future dates. + IF !reminder_date || reminder_date == '1970-01-01'; + reminder_date = today; + END; +%] + +

+ Choose when you want to be reminded about supporting Bugzilla: +

+ + + + + + + + + + +
+ +
+ + diff --git a/template/en/default/account/prefs/prefs.html.tmpl b/template/en/default/account/prefs/prefs.html.tmpl index 6b97b08cfc..c4cfee7cf2 100644 --- a/template/en/default/account/prefs/prefs.html.tmpl +++ b/template/en/default/account/prefs/prefs.html.tmpl @@ -60,6 +60,12 @@ link => "userprefs.cgi?tab=settings", saveable => "1" }, + { + name => "donate", + label => "Donate to Bugzilla", + link => "userprefs.cgi?tab=donate", + saveable => "1" + }, { name => "email", label => "Email Preferences", @@ -98,6 +104,13 @@ saveable => "0" }, ]; + IF Param('donation_banner_visibility') == 'disabled'; + visible_tabs = []; + FOREACH tab IN tabs; + visible_tabs.push(tab) IF tab.name != 'donate'; + END; + tabs = visible_tabs; + END; Hook.process('tabs'); FOREACH tab IN tabs; diff --git a/template/en/default/admin/params/donation.html.tmpl b/template/en/default/admin/params/donation.html.tmpl new file mode 100644 index 0000000000..8bbd200d02 --- /dev/null +++ b/template/en/default/admin/params/donation.html.tmpl @@ -0,0 +1,20 @@ +[%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. + # + # This Source Code Form is "Incompatible With Secondary Licenses", as + # defined by the Mozilla Public License, v. 2.0. + #%] +[% + title = "Donation" + desc = "Configure the Bugzilla donation banner" +%] + +[% param_descs = { + donation_banner_visibility => + "Controls which logged-in users can see the donation banner on the " + _ "$terms.Bugzilla home page." + _ " ", +} %] diff --git a/template/en/default/index.html.tmpl b/template/en/default/index.html.tmpl index 5e7098dc76..4e85362260 100644 --- a/template/en/default/index.html.tmpl +++ b/template/en/default/index.html.tmpl @@ -32,8 +32,53 @@ no_yui = 1 %] +
+ +[% IF donation %] +
+
+ Buggie holding donation funds placeholder + +
+ [% IF donation.show_thanks %] +

Thanks for upgrading Bugzilla!

+ [% END %] + +

[% donation.message FILTER html %]

+ +

+ Donate to Bugzilla +

+ +
+ + + + +
+ + [% IF donation.visibility_note %] +

[% donation.visibility_note FILTER html %] + You can configure this notification from the + Parameters page.

+ [% END %] +
+
+
+[% END %] + [% IF release %] -
+
[% IF release.data %] [% IF release.eos_date %]

[% terms.Bugzilla %] [%+ release.branch_version FILTER html %] will @@ -83,7 +128,6 @@

[% END %] -
diff --git a/userprefs.cgi b/userprefs.cgi index d7a5eb146a..561ae3826e 100755 --- a/userprefs.cgi +++ b/userprefs.cgi @@ -29,6 +29,14 @@ use DateTime; use constant SESSION_MAX => 20; +# Donation banner settings are managed from the dedicated "Donate" tab (and the +# home page banner), not the generic Settings tab. Two of them hold internal +# state (last shown version, reminder date) with no legal_values, so they would +# otherwise render as empty dropdowns and be reset to their defaults whenever the +# Settings tab is saved. +use constant DONATION_SETTINGS => + qw(donate_banner_pref donate_banner_last_version donate_banner_reminder_date); + local our $template = Bugzilla->template; local our $vars = {}; @@ -230,10 +238,12 @@ sub DoSettings { my $user = Bugzilla->user; my %settings; + my %is_donation_setting = map { $_ => 1 } DONATION_SETTINGS; my $has_settings_enabled = 0; foreach my $name (sort keys %{$user->settings}) { my $setting = $user->settings->{$name}; next if !$setting->{is_enabled}; + next if $is_donation_setting{$name}; my $category = $setting->{category}; $settings{$category} ||= []; push(@{$settings{$category}}, $setting); @@ -245,6 +255,45 @@ sub DoSettings { $vars->{dont_show_button} = !$has_settings_enabled; } +sub DoDonate { + my $user = Bugzilla->user; + + $vars->{settings} = $user->settings; + + # Default the reminder date picker to today rather than the stored epoch + # sentinel, since a reminder is always meant to be a future date. + $vars->{today} = DateTime->now(time_zone => Bugzilla->local_timezone)->ymd; +} + +sub SaveDonate { + my $cgi = Bugzilla->cgi; + my $user = Bugzilla->user; + my $settings = $user->settings; + + my $pref = $cgi->param('donate_banner_pref') || 'next_upgrade'; + my $date = $cgi->param('donate_banner_reminder_date') || ''; + + if ($pref eq 'specific_date') { + validate_date($date) + || ThrowUserError('illegal_date', {date => $date, format => 'YYYY-MM-DD'}); + $settings->{'donate_banner_reminder_date'}->set($date); + } + elsif ($pref ne 'next_upgrade' && $pref ne 'never') { + ThrowCodeError('setting_value_invalid', + {name => 'donate_banner_pref', value => $pref}); + } + + $settings->{'donate_banner_pref'}->set($pref); + + # Stamp the current version so that the "next upgrade" reminder is dismissed + # until Bugzilla is actually upgraded, matching the home page banner. Without + # this, get_banner() still sees last_version != current and keeps showing. + $settings->{'donate_banner_last_version'}->set(BUGZILLA_VERSION); + + clear_settings_cache($user->id); + $vars->{settings} = $user->settings(1); +} + sub SaveSettings { my $cgi = Bugzilla->cgi; my $user = Bugzilla->user; @@ -253,8 +302,11 @@ sub SaveSettings { my @setting_list = keys %$settings; my $mfa_event = undef; + my %is_donation_setting = map { $_ => 1 } DONATION_SETTINGS; + foreach my $name (@setting_list) { next if !($settings->{$name}->{'is_enabled'}); + next if $is_donation_setting{$name}; my $value = $cgi->param($name); next unless defined $value; my $setting = new Bugzilla::User::Setting($name); @@ -1037,6 +1089,11 @@ SWITCH: for ($current_tab_name) { DoSettings(); last SWITCH; }; + /^donate$/ && do { + SaveDonate() if $save_changes; + DoDonate(); + last SWITCH; + }; /^email$/ && do { SaveEmail() if $save_changes; DoEmail();