diff --git a/extensions/BugzillaDonationBanner/Config.pm b/extensions/BugzillaDonationBanner/Config.pm new file mode 100644 index 000000000..9a2d77639 --- /dev/null +++ b/extensions/BugzillaDonationBanner/Config.pm @@ -0,0 +1,16 @@ +# 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::Extension::BugzillaDonationBanner; + +use 5.14.0; +use strict; +use warnings; + +use constant NAME => 'BugzillaDonationBanner'; + +__PACKAGE__->NAME; diff --git a/extensions/BugzillaDonationBanner/Extension.pm b/extensions/BugzillaDonationBanner/Extension.pm new file mode 100644 index 000000000..6854f13c8 --- /dev/null +++ b/extensions/BugzillaDonationBanner/Extension.pm @@ -0,0 +1,124 @@ +# 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::Extension::BugzillaDonationBanner; + +use 5.14.0; +use strict; +use warnings; +use base qw(Bugzilla::Extension); + +use Bugzilla::Constants; +use Bugzilla::Install::Filesystem; +use Bugzilla::User::Setting qw(add_setting); +use Bugzilla::Extension::BugzillaDonationBanner::Donation; + +our $VERSION = '1.0'; + +sub config_add_panels { + my ($self, $args) = @_; + $args->{panel_modules}->{Donation} + = 'Bugzilla::Extension::BugzillaDonationBanner::Config'; +} + +sub install_before_final_checks { + add_setting({ + name => 'donate_banner_pref', + options => ['next_upgrade', 'specific_date', 'never'], + default => 'next_upgrade', + category => 'Bugzilla.org', + }); + add_setting({ + name => 'donate_banner_last_version', + default => '0.0', + category => 'Bugzilla.org', + }); + add_setting({ + name => 'donate_banner_reminder_date', + default => '1970-01-01', + category => 'Bugzilla.org', + }); +} + +sub install_filesystem { + my ($self, $args) = @_; + my $files = $args->{files}; + my $extension_dir = bz_locations()->{extensionsdir} . '/' + . __PACKAGE__->NAME; + + $files->{"$extension_dir/bin/donate.cgi"} + = {perms => Bugzilla::Install::Filesystem::WS_EXECUTE}; + $files->{"$extension_dir/web/buggie_watering.svg"} + = {perms => Bugzilla::Install::Filesystem::WS_SERVE}; + $files->{"$extension_dir/web/donation-banner.css"} + = {perms => Bugzilla::Install::Filesystem::WS_SERVE}; + $files->{"$extension_dir/web/donation-banner.js"} + = {perms => Bugzilla::Install::Filesystem::WS_SERVE}; +} + +sub app_startup { + my ($self, $args) = @_; + my $app = $args->{app}; + my $r = $app->routes; + + Bugzilla::App::CGI->load_one( + 'bugzilla_donation_banner_cgi', + 'extensions/BugzillaDonationBanner/bin/donate.cgi' + ); + $r->post('/extensions/BugzillaDonationBanner/bin/donate.cgi') + ->to('CGI#bugzilla_donation_banner_cgi'); +} + +sub template_before_process { + my ($self, $args) = @_; + my ($file, $vars) = @$args{qw(file vars)}; + + if ($file =~ m{(?:^|/)index\.html\.tmpl$}) { + $vars->{donation} + = Bugzilla::Extension::BugzillaDonationBanner::Donation::get_banner(); + } +} + +sub user_preferences_settings { + my ($self, $args) = @_; + my $skip_settings = $args->{skip_settings}; + $skip_settings->{donate_banner_pref} = 1; + $skip_settings->{donate_banner_last_version} = 1; + $skip_settings->{donate_banner_reminder_date} = 1; +} + +sub user_preferences { + my ($self, $args) = @_; + return unless $args->{current_tab} eq 'donate'; + + my $vars = $args->{vars}; + my $user = Bugzilla->user; + my $cgi = Bugzilla->cgi; + + if ($args->{save_changes}) { + my $pref = $cgi->param('donate_banner_pref') || 'next_upgrade'; + my $date = $cgi->param('donate_banner_reminder_date') || ''; + Bugzilla::Extension::BugzillaDonationBanner::Donation::save_preference( + $pref, $date); + $vars->{changes_saved} = 1; + } + + $vars->{settings} = $user->settings(1); + $vars->{donation} + = Bugzilla::Extension::BugzillaDonationBanner::Donation::get_banner(1); + $vars->{donate_pref} = $user->setting('donate_banner_pref'); + $vars->{today} + = Bugzilla::Extension::BugzillaDonationBanner::Donation::today(); + my $reminder_date = $user->setting('donate_banner_reminder_date'); + $vars->{reminder_date} + = !$reminder_date || $reminder_date eq '1970-01-01' + ? $vars->{today} + : $reminder_date; + ${$args->{handled}} = 1; +} + +__PACKAGE__->NAME; diff --git a/extensions/BugzillaDonationBanner/bin/donate.cgi b/extensions/BugzillaDonationBanner/bin/donate.cgi new file mode 100644 index 000000000..aaa5e5bfc --- /dev/null +++ b/extensions/BugzillaDonationBanner/bin/donate.cgi @@ -0,0 +1,29 @@ +#!/usr/bin/env perl + +use 5.14.0; +use strict; +use warnings; + +use lib qw(../../.. ../../../lib); + +use Bugzilla; +use Bugzilla::Constants; +use Bugzilla::Error; +use Bugzilla::Token qw(check_hash_token); +use Bugzilla::Extension::BugzillaDonationBanner::Donation; + +my $cgi = Bugzilla->cgi; +if ($cgi->request_method ne 'POST') { + ThrowCodeError('illegal_request_method', + {method => $cgi->request_method, accepted => ['POST']}); +} + +Bugzilla->login(LOGIN_REQUIRED) unless Bugzilla->user->id; +check_hash_token($cgi->param('token'), ['donation_banner']); + +Bugzilla::Extension::BugzillaDonationBanner::Donation::dismiss( + scalar $cgi->param('donate_action'), + scalar $cgi->param('donate_banner_reminder_date'), +); + +$cgi->base_redirect(); diff --git a/extensions/BugzillaDonationBanner/lib/Config.pm b/extensions/BugzillaDonationBanner/lib/Config.pm new file mode 100644 index 000000000..1193b338c --- /dev/null +++ b/extensions/BugzillaDonationBanner/lib/Config.pm @@ -0,0 +1,28 @@ +# 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::Extension::BugzillaDonationBanner::Config; + +use 5.14.0; +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; diff --git a/extensions/BugzillaDonationBanner/lib/Donation.pm b/extensions/BugzillaDonationBanner/lib/Donation.pm new file mode 100644 index 000000000..e6a5c21e0 --- /dev/null +++ b/extensions/BugzillaDonationBanner/lib/Donation.pm @@ -0,0 +1,130 @@ +# 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::Extension::BugzillaDonationBanner::Donation; + +use 5.14.0; +use strict; +use warnings; + +use Bugzilla::Constants; +use Bugzilla::Error; +use Bugzilla::Token qw(issue_hash_token); +use Bugzilla::User::Setting qw(clear_settings_cache); +use Bugzilla::Util qw(validate_date); + +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 today { + return DateTime->now(time_zone => Bugzilla->local_timezone)->ymd; +} + +sub get_banner { + my ($ignore_preference) = @_; + 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 $reminder_date + = $settings->{donate_banner_reminder_date}->{value} || '1970-01-01'; + my $current_version = BUGZILLA_VERSION; + + my ($show, $show_thanks) = (0, 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') { + $show = $reminder_date le today(); + } + + return undef if !$show && !$ignore_preference; + + my @messages = BANNER_MESSAGES; + my $data = { + url => BANNER_URL, + message => $messages[int(rand(@messages))], + show_thanks => $show_thanks, + token => issue_hash_token(['donation_banner']), + today => today(), + settings_link => 'editparams.cgi?section=donation#donation_banner_visibility_desc', + }; + + 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 save_preference { + my ($pref, $date) = @_; + my $user = Bugzilla->user; + my $settings = $user->settings; + + 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') { + ThrowUserError('invalid_parameter', { + name => 'donate_banner_pref', + err => "must be one of 'next_upgrade', 'specific_date', or 'never'", + }); + } + + $settings->{donate_banner_pref}->set($pref); + $settings->{donate_banner_last_version}->set(BUGZILLA_VERSION); + clear_settings_cache($user->id); +} + +sub dismiss { + my ($action, $date) = @_; + my $settings = Bugzilla->user->settings; + + if ($action eq 'next_upgrade') { + save_preference('next_upgrade', ''); + } + elsif ($action eq 'week' || $action eq 'month') { + my $dt = DateTime->now(time_zone => Bugzilla->local_timezone); + $dt->add(days => $action eq 'week' ? 7 : 30); + save_preference('specific_date', $dt->ymd); + } + elsif ($action eq 'date') { + save_preference('specific_date', $date); + } + elsif ($action eq 'never') { + save_preference('never', ''); + } +} + +1; diff --git a/extensions/BugzillaDonationBanner/template/en/default/account/prefs/donate.html.tmpl b/extensions/BugzillaDonationBanner/template/en/default/account/prefs/donate.html.tmpl new file mode 100644 index 000000000..6bb151ab2 --- /dev/null +++ b/extensions/BugzillaDonationBanner/template/en/default/account/prefs/donate.html.tmpl @@ -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/. + #%] + +[% PROCESS "donation/banner.html.tmpl" show_dismissal=0 %] + +

Choose when you want to be reminded about supporting Bugzilla on the home page:

+ + + + + + + + + + +
+ +
diff --git a/extensions/BugzillaDonationBanner/template/en/default/admin/params/donation.html.tmpl b/extensions/BugzillaDonationBanner/template/en/default/admin/params/donation.html.tmpl new file mode 100644 index 000000000..d2aa5dbcf --- /dev/null +++ b/extensions/BugzillaDonationBanner/template/en/default/admin/params/donation.html.tmpl @@ -0,0 +1,11 @@ +[%# 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/. + #%] + +[% 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 Bugzilla home page.' + } +%] diff --git a/extensions/BugzillaDonationBanner/template/en/default/donation/banner.html.tmpl b/extensions/BugzillaDonationBanner/template/en/default/donation/banner.html.tmpl new file mode 100644 index 000000000..9396e435d --- /dev/null +++ b/extensions/BugzillaDonationBanner/template/en/default/donation/banner.html.tmpl @@ -0,0 +1,51 @@ +[% IF donation %] +
+
+ Buggie watering a plant +
+ [% IF donation.show_thanks %] +

Thanks for upgrading Bugzilla!

+ [% END %] +

[% donation.message FILTER html %]

+

+ Donate to Bugzilla +

+ [% IF show_dismissal %] +
+ Remind me later or dismiss this message +
+ + + + + +
+

+ You can change this choice later from your + Donate to Bugzilla preferences. +

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

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

+ [% END %] +
+
+
+[% END %] diff --git a/extensions/BugzillaDonationBanner/template/en/default/hook/account/prefs/prefs-tabs.html.tmpl b/extensions/BugzillaDonationBanner/template/en/default/hook/account/prefs/prefs-tabs.html.tmpl new file mode 100644 index 000000000..5bb5ffb6a --- /dev/null +++ b/extensions/BugzillaDonationBanner/template/en/default/hook/account/prefs/prefs-tabs.html.tmpl @@ -0,0 +1,13 @@ +[%# 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/. + #%] + +[% UNLESS Param('donation_banner_visibility') == 'disabled' %] + [% CALL tabs.import([{ + name => 'donate', + label => 'Donate to Bugzilla', + link => 'userprefs.cgi?tab=donate', + saveable => 1 + }]) %] +[% END %] diff --git a/extensions/BugzillaDonationBanner/template/en/default/hook/global/header-additional_header.html.tmpl b/extensions/BugzillaDonationBanner/template/en/default/hook/global/header-additional_header.html.tmpl new file mode 100644 index 000000000..ece94dfd9 --- /dev/null +++ b/extensions/BugzillaDonationBanner/template/en/default/hook/global/header-additional_header.html.tmpl @@ -0,0 +1,2 @@ + + diff --git a/extensions/BugzillaDonationBanner/template/en/default/hook/index-intro.html.tmpl b/extensions/BugzillaDonationBanner/template/en/default/hook/index-intro.html.tmpl new file mode 100644 index 000000000..e60b50f05 --- /dev/null +++ b/extensions/BugzillaDonationBanner/template/en/default/hook/index-intro.html.tmpl @@ -0,0 +1,6 @@ +[%# 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/. + #%] + +[% PROCESS "donation/banner.html.tmpl" show_dismissal=1 %] diff --git a/extensions/BugzillaDonationBanner/web/buggie_watering.svg b/extensions/BugzillaDonationBanner/web/buggie_watering.svg new file mode 100644 index 000000000..18210041f --- /dev/null +++ b/extensions/BugzillaDonationBanner/web/buggie_watering.svg @@ -0,0 +1,1888 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/extensions/BugzillaDonationBanner/web/donation-banner.css b/extensions/BugzillaDonationBanner/web/donation-banner.css new file mode 100644 index 000000000..e328084a7 --- /dev/null +++ b/extensions/BugzillaDonationBanner/web/donation-banner.css @@ -0,0 +1,77 @@ +.home_banner { + box-sizing: border-box; + max-width: 640px; + margin: 1em auto; + padding: 0.5em 1em; + border-width: 2px; + border-style: solid; + border-radius: 8px; +} + +#new_release { + border-color: rgb(var(--accent-color-red-1)); + font-weight: bold; +} + +.home_banner .notice { + font-size: var(--font-size-small); + font-weight: normal; +} + +.donation_banner_preferences { + margin-left: 0; + margin-right: 0; +} + +.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; + 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; font-weight: bold; } +.donation_banner_message { margin: 0 0 0.6rem; 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 { background: rgba(var(--accent-color-green-1), 0.9); text-decoration: none; } +.donation_banner_dismissal { margin-top: 0.75rem; } +.donation_banner_dismissal summary { cursor: pointer; color: var(--secondary-label-color); } +.donation_banner_dismissal[open] summary { margin-bottom: 0.5rem; } +.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_date_field { display: inline-flex; align-items: center; gap: 0.35rem; } +.donation_banner_date_field[hidden] { display: none; } +.donation_banner_submit { padding: 0.35rem 0.8rem; } +.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; } +.donation_banner_preferences_note { margin: 0.5rem 0 0; font-size: 0.7rem; color: var(--secondary-label-color); } + +@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.7); } + .donation_banner_cta { border-color: rgb(var(--accent-color-lightgreen-1)); background: rgb(var(--accent-color-lightgreen-1)); } + .donation_banner_cta:hover { background: rgba(var(--accent-color-lightgreen-1), 0.85); } + .donation_banner_form_subtle { opacity: 1; } +} diff --git a/extensions/BugzillaDonationBanner/web/donation-banner.js b/extensions/BugzillaDonationBanner/web/donation-banner.js new file mode 100644 index 000000000..c6e79f65c --- /dev/null +++ b/extensions/BugzillaDonationBanner/web/donation-banner.js @@ -0,0 +1,25 @@ +(function() { + var action = document.getElementById('donate_action') || + document.getElementById('donate_banner_pref'); + var field = document.getElementById('donate_banner_date_field') || + document.getElementById('donate_date_row'); + var date = document.getElementById('donate_banner_reminder_date'); + + if (!action || !field || !date) { + return; + } + + function toggle() { + var showDate = action.value === 'date' || action.value === 'specific_date'; + if (field.id === 'donate_banner_date_field') { + field.hidden = !showDate; + } + else { + field.style.display = showDate ? '' : 'none'; + } + date.required = showDate; + } + + action.addEventListener('change', toggle); + toggle(); +})(); diff --git a/userprefs.cgi b/userprefs.cgi index 70c4a1636..4765d8b2b 100755 --- a/userprefs.cgi +++ b/userprefs.cgi @@ -230,10 +230,14 @@ sub DoSettings { my $user = Bugzilla->user; my %settings; + my %skip_settings; + Bugzilla::Hook::process('user_preferences_settings', + {skip_settings => \%skip_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 $skip_settings{$name}; my $category = $setting->{category}; $settings{$category} ||= []; push(@{$settings{$category}}, $setting); @@ -253,8 +257,13 @@ sub SaveSettings { my @setting_list = keys %$settings; my $mfa_event = undef; + my %skip_settings; + Bugzilla::Hook::process('user_preferences_settings', + {skip_settings => \%skip_settings}); + foreach my $name (@setting_list) { next if !($settings->{$name}->{'is_enabled'}); + next if $skip_settings{$name}; my $value = $cgi->param($name); next unless defined $value; my $setting = new Bugzilla::User::Setting($name);